Skip to content

Commit

Permalink
Support credential-scopes (#801)
Browse files Browse the repository at this point in the history
* Support credential-scopes

* Update core-http version

* Update test
  • Loading branch information
joheredi authored Dec 4, 2020
1 parent c8e68b8 commit 28ebb6e
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 16 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@azure-tools/autorest-extension-base": "^3.1.246",
"@azure-tools/codegen": "^2.4.267",
"@azure-tools/codemodel": "^4.13.339",
"@azure/core-http": "^1.2.1-alpha.20201113.2",
"@azure/core-http": "^1.2.1-alpha.20201203.2",
"@azure/core-lro": "^1.0.1",
"@azure/core-paging": "^1.1.3",
"@azure/core-tracing": "1.0.0-preview.9",
Expand Down
29 changes: 27 additions & 2 deletions src/generators/clientContextFileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ function writeConstructorBody(
writeStatement(
writeDefaultOptions(
clientParams.some(p => p.name === "credentials"),
hasLRO
hasLRO,
clientDetails
)
),
writeStatement(getEndpointStatement(clientDetails.endpoint), addBlankLine),
Expand Down Expand Up @@ -173,7 +174,29 @@ const writeStatements = (lines: string[], shouldAddBlankLine = false) => (
shouldAddBlankLine && writer.blankLine();
};

function writeDefaultOptions(hasCredentials: boolean, hasLRO: boolean) {
function getCredentialScopesValue(credentialScopes?: string | string[]) {
if (Array.isArray(credentialScopes)) {
return `[${credentialScopes.map(scope => `"${scope}"`).join()}]`;
} else if (typeof credentialScopes === "string") {
return `"${credentialScopes}"`;
}

return credentialScopes;
}

function writeDefaultOptions(
hasCredentials: boolean,
hasLRO: boolean,
clientDetails: ClientDetails
) {
const credentialScopes = getCredentialScopesValue(
clientDetails.options.credentialScopes
);
const addScopes = credentialScopes
? `if(!options.credentialScopes) {
options.credentialScopes = ${credentialScopes}
}`
: "";
const addLROPolicy = hasLRO
? `
// Building the request policy fatories based on the passed factories and the
Expand Down Expand Up @@ -212,6 +235,8 @@ function writeDefaultOptions(hasCredentials: boolean, hasLRO: boolean) {
options.userAgent = \`\${packageName}/\${packageVersion} \${defaultUserAgent}\`;
}
${addScopes}
${addLROPolicy}
super(${hasCredentials ? "credentials" : `undefined`}, options);
Expand Down
1 change: 1 addition & 0 deletions src/models/clientDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ClientOptions {
disablePagingAsyncIterators?: boolean;
mediaTypes?: Set<KnownMediaType>;
hasPaging?: boolean;
credentialScopes?: string[];
}

export interface TracingInfo {
Expand Down
46 changes: 41 additions & 5 deletions src/transforms/optionsTransforms.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Host } from "@azure-tools/autorest-extension-base";
import { Channel, Host } from "@azure-tools/autorest-extension-base";
import { ClientOptions } from "../models/clientDetails";
import { OperationGroupDetails } from "../models/operationDetails";
import { KnownMediaType } from "@azure-tools/codegen";
Expand All @@ -8,15 +8,21 @@ export async function transformOptions(
operationGroups: OperationGroupDetails[]
): Promise<ClientOptions> {
const mediaTypes = getMediaTypesStyles(operationGroups);
const addCredentials = !((await host.GetValue("add-credentials")) === false);
const azureArm = !((await host.GetValue("azure-arm")) === false);

const addCredentials =
!((await host.GetValue("add-credentials")) === false) || azureArm;

const disablePagingAsyncIterators =
(await host.GetValue("disable-async-iterators")) === true;
const credentialScopes = await getCredentialScopes(host);

return {
addCredentials,
mediaTypes,
disablePagingAsyncIterators,
hasPaging: hasPagingOperations(operationGroups)
hasPaging: hasPagingOperations(operationGroups),
credentialScopes
};
}

Expand All @@ -36,6 +42,36 @@ function hasPagingOperations(operationGroups: OperationGroupDetails[]) {
return operationGroups.some(og => og.operations.some(o => !!o.pagination));
}

function hasLroOperations(operationGroups: OperationGroupDetails[]) {
return operationGroups.some(og => og.operations.some(o => o.isLRO));
export async function getCredentialScopes(
host: Host
): Promise<string[] | undefined> {
const addCredentials = await host.GetValue("add-credentials");
const credentialScopes = await host.GetValue("credential-scopes");
const azureArm = await host.GetValue("azure-arm");

if (credentialScopes && !addCredentials) {
throw new Error(
"--credential-scopes must be used with the --add-credentials flag"
);
}

if (!credentialScopes) {
if (azureArm) {
return ["https://management.azure.com/.default"];
} else if (addCredentials) {
host.Message({
Channel: Channel.Warning,
Text: `You have default credential policy BearerTokenCredentialPolicy
but not the --credential-scopes flag set while generating non-management plane code.
This is not recommended because it forces the customer to pass credential scopes
through kwargs if they want to authenticate.`
});
}
}

if (typeof credentialScopes === "string") {
return credentialScopes.split(",");
}

return undefined;
}
10 changes: 8 additions & 2 deletions test/integration/azureSpecialProperties.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ import {
HttpHeaders
} from "@azure/core-http";

describe.only("auth validation", () => {
describe("auth validation", () => {
it("should add authorization header", async () => {
const expectedScopes = [
"https://microsoft.com/.default",
"http://microsoft.com/.default"
];

const mockCredential: TokenCredential = {
getToken: async _scopes => {
getToken: async scopes => {
assert.deepEqual(scopes, expectedScopes);
return {
token: "test-token",
expiresOnTimestamp: 111111
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ export class AzureSpecialPropertiesClientContext extends coreHttp.ServiceClient
options.userAgent = `${packageName}/${packageVersion} ${defaultUserAgent}`;
}

if (!options.credentialScopes) {
options.credentialScopes = [
"https://microsoft.com/.default",
"http://microsoft.com/.default"
];
}

super(credentials, options);

this.requestContentType = "application/json; charset=utf-8";
Expand Down
123 changes: 123 additions & 0 deletions test/unit/transforms/optionsTransforms.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Channel, Host, Message } from "@azure-tools/autorest-extension-base";
import { assert } from "chai";
import { getCredentialScopes } from "../../../src/transforms/optionsTransforms";

describe("transformOptions", () => {
describe("getCredentialScopes", () => {
it("should throw an error if credentials is false but credential-scopes are provided", async () => {
const mockHost = {
GetValue: async (key: string) => {
switch (key) {
case "add-credentials":
return false;
case "credential-scopes":
return "https://microsoft.com/.default";
case "azure-arm":
return false;
default:
return undefined;
}
},
Message: (message: Message) => {}
} as Host;
try {
await getCredentialScopes(mockHost);
assert.fail("Expected to throw");
} catch (error) {
assert.include(
error.message,
"--credential-scopes must be used with the --add-credentials flag"
);
}
});

it("should set default scopes when isArm is set and no credential-scopes were passed", async () => {
const mockHost = {
GetValue: async (key: string) => {
switch (key) {
case "add-credentials":
return true;
case "credential-scopes":
return undefined;
case "azure-arm":
return true;
default:
return undefined;
}
},
Message: (message: Message) => {}
} as Host;
const scopes = await getCredentialScopes(mockHost);
assert.deepEqual(scopes, ["https://management.azure.com/.default"]);
});

it("should log a warning when credentials is true but no scopes are passed", async () => {
const mockHost = {
GetValue: async (key: string) => {
switch (key) {
case "add-credentials":
return true;
case "credential-scopes":
return undefined;
case "azure-arm":
return false;
default:
return undefined;
}
},
Message: (message: Message) => {
assert.include(
message.Text,
"You have default credential policy BearerTokenCredentialPolicy"
);
assert.equal(message.Channel, Channel.Warning);
}
} as Host;
const scopes = await getCredentialScopes(mockHost);
assert.equal(scopes, undefined);
});

it("should handle a single credential scope", async () => {
const mockHost = {
GetValue: async (key: string) => {
switch (key) {
case "add-credentials":
return true;
case "credential-scopes":
return "https://microsoft.com/.defaults";
case "azure-arm":
return false;
default:
return undefined;
}
},
Message: (message: Message) => {}
} as Host;
const scopes = await getCredentialScopes(mockHost);
assert.deepEqual(scopes, ["https://microsoft.com/.defaults"]);
});

it("should handle a multiple credential scopes", async () => {
const mockHost = {
GetValue: async (key: string) => {
switch (key) {
case "add-credentials":
return true;
case "credential-scopes":
return "https://microsoft.com/.defaults,http://microsoft.com/.defaults";
case "azure-arm":
return false;
default:
return undefined;
}
},
Message: (message: Message) => {}
} as Host;
const scopes = await getCredentialScopes(mockHost);
assert.deepEqual(scopes, [
"https://microsoft.com/.defaults",
"http://microsoft.com/.defaults"
]);
});
});
});
14 changes: 11 additions & 3 deletions test/utils/test-swagger-gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface SwaggerConfig {
packageName: string;
addCredentials?: boolean;
licenseHeader?: boolean;
credentialScopes?: string;
tracing?: TracingInfo;
disableAsyncIterators?: boolean;
}
Expand Down Expand Up @@ -44,7 +45,9 @@ const testSwaggers: { [name: string]: SwaggerConfig } = {
clientName: "AzureSpecialPropertiesClient",
packageName: "azure-special-properties",
licenseHeader: true,
addCredentials: true
addCredentials: true,
credentialScopes:
"https://microsoft.com/.default,http://microsoft.com/.default"
},
bodyArray: {
swagger: "body-array.json",
Expand Down Expand Up @@ -373,7 +376,8 @@ const generateSwaggers = async (
packageName,
licenseHeader,
tracing,
disableAsyncIterators
disableAsyncIterators,
credentialScopes
} = testSwaggers[name];

let swaggerPath = swagger;
Expand All @@ -382,6 +386,10 @@ const generateSwaggers = async (
? `--tracing-info.namespace=${tracing.namespace} --tracing-info.packagePrefix=${tracing.packagePrefix}`
: "";

const credentialScopesInfo = credentialScopes
? `--credential-scopes=${credentialScopes}`
: "";

const disableIterators = disableAsyncIterators
? "--disable-async-iterators=true"
: "";
Expand All @@ -391,7 +399,7 @@ const generateSwaggers = async (
swaggerPath = `node_modules/@microsoft.azure/autorest.testserver/swagger/${swagger}`;
}

let autorestCommand = `autorest --clear-output-folder=true ${tracingInfo} ${disableIterators} --license-header=${!!licenseHeader} --add-credentials=${!!addCredentials} --typescript --output-folder=./test/integration/generated/${name} --use=. --title=${clientName} --input-file=${swaggerPath} --package-name=${packageName} --package-version=${package_version}`;
let autorestCommand = `autorest --clear-output-folder=true ${tracingInfo} ${disableIterators} ${credentialScopesInfo} --license-header=${!!licenseHeader} --add-credentials=${!!addCredentials} --typescript --output-folder=./test/integration/generated/${name} --use=. --title=${clientName} --input-file=${swaggerPath} --package-name=${packageName} --package-version=${package_version}`;

if (isDebugging) {
autorestCommand = `${autorestCommand} --typescript.debugger`;
Expand Down

0 comments on commit 28ebb6e

Please sign in to comment.