diff --git a/.azure-pipelines/common/test.yml b/.azure-pipelines/common/test.yml index 43716d2..7f9e4d9 100644 --- a/.azure-pipelines/common/test.yml +++ b/.azure-pipelines/common/test.yml @@ -11,13 +11,13 @@ steps: condition: in(variables['agent.os'], 'Windows_NT') displayName: 'Use Python on Windows' # specific version for Windows: https://github.com/actions/virtual-environments/blob/main/images/win/Windows2019-Readme.md inputs: - versionSpec: 3.9.9 + versionSpec: 3.9.13 - task: UsePythonVersion@0 condition: in(variables['agent.os'], 'Darwin', 'Linux') displayName: 'Use Python 3.9.10' # specific version for macOS: https://github.com/actions/virtual-environments/blob/main/images/macos/macos-11-Readme.md inputs: - versionSpec: 3.9.10 + versionSpec: 3.9.13 - task: Npm@1 displayName: 'Test' diff --git a/aspnetcorerazor.json b/aspnetcorerazor.json index 2f2d2cf..a9d6d66 100644 --- a/aspnetcorerazor.json +++ b/aspnetcorerazor.json @@ -509,5 +509,12 @@ "\t", "" ] + }, + "get-authorization-context": { + "prefix": "get-authorization-context", + "description": "Gets the authorization context of the specified authorization, including the access token", + "body": [ + "" + ] } } \ No newline at end of file diff --git a/package.json b/package.json index 38ad955..3a31cb6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-apimanagement", "displayName": "Azure API Management", "description": "An Azure API Management extension for Visual Studio Code.", - "version": "1.0.3", + "version": "1.0.4", "publisher": "ms-azuretools", "icon": "resources/apim-icon-newone.png", "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", @@ -74,7 +74,19 @@ "onCommand:azureApiManagement.revisions", "onCommand:azureApiManagement.setCustomHostName", "onCommand:azureApiManagement.createSubscription", - "onCommand:azureApiManagement.deleteSubscription" + "onCommand:azureApiManagement.deleteSubscription", + "onCommand:azureApiManagement.createAuthorizationProvider", + "onCommand:azureApiManagement.deleteAuthorizationProvider", + "onCommand:azureApiManagement.copyAuthorizationProviderRedirectUrl", + "onCommand:azureApiManagement.createAuthorization", + "onCommand:azureApiManagement.authorizeAuthorization", + "onCommand:azureApiManagement.deleteAuthorization", + "onCommand:azureApiManagement.createAuthorizationAccessPolicy", + "onCommand:azureApiManagement.deleteAuthorizationAccessPolicy", + "onCommand:azureApiManagement.copyAuthorizationPolicy", + "onCommand:azureApiManagement.showArmAuthorizationProvider", + "onCommand:azureApiManagement.showArmAuthorization", + "onCommand:azureApiManagement.showArmAuthorizationAccessPolicy" ], "main": "main", "contributes": { @@ -385,6 +397,66 @@ "light": "resources/light/diff.svg", "dark": "resources/dark/diff.svg" } + }, + { + "command": "azureApiManagement.createAuthorizationProvider", + "title": "%azureApiManagement.createAuthorizationProvider%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.deleteAuthorizationProvider", + "title": "%azureApiManagement.deleteAuthorizationProvider%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.copyAuthorizationProviderRedirectUrl", + "title": "%azureApiManagement.copyAuthorizationProviderRedirectUrl%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.createAuthorization", + "title": "%azureApiManagement.createAuthorization%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.authorizeAuthorization", + "title": "%azureApiManagement.authorizeAuthorization%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.deleteAuthorization", + "title": "%azureApiManagement.deleteAuthorization%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.copyAuthorizationPolicy", + "title": "%azureApiManagement.copyAuthorizationPolicy%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.createAuthorizationAccessPolicy", + "title": "%azureApiManagement.createAuthorizationAccessPolicy%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.deleteAuthorizationAccessPolicy", + "title": "%azureApiManagement.deleteAuthorizationAccessPolicy%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.showArmAuthorizationProvider", + "title": "%azureApiManagement.showArmAuthorizationProvider%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.showArmAuthorization", + "title": "%azureApiManagement.showArmAuthorization%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.showArmAuthorizationAccessPolicy", + "title": "%azureApiManagement.showArmAuthorizationAccessPolicy%", + "category": "Azure API Management" } ], "viewsContainers": { @@ -635,6 +707,66 @@ "command": "azureApiManagement.deleteSubscription", "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementSubscriptionTreeItem", "group": "1@1" + }, + { + "command": "azureApiManagement.createAuthorizationProvider", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorizationProviders", + "group": "1@1" + }, + { + "command": "azureApiManagement.Refresh", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorizationProviders", + "group": "1@2" + }, + { + "command": "azureApiManagement.copyAuthorizationProviderRedirectUrl", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorizationProvider", + "group": "1@1" + }, + { + "command": "azureApiManagement.deleteAuthorizationProvider", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorizationProvider", + "group": "1@2" + }, + { + "command": "azureApiManagement.createAuthorization", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorizations", + "group": "1@1" + }, + { + "command": "azureApiManagement.Refresh", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorizations", + "group": "1@2" + }, + { + "command": "azureApiManagement.authorizeAuthorization", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorization", + "group": "1@1" + }, + { + "command": "azureApiManagement.copyAuthorizationPolicy", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorization", + "group": "1@2" + }, + { + "command": "azureApiManagement.deleteAuthorization", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorization", + "group": "1@3" + }, + { + "command": "azureApiManagement.createAuthorizationAccessPolicy", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorizationAccessPolicies", + "group": "1@1" + }, + { + "command": "azureApiManagement.Refresh", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorizationAccessPolicies", + "group": "1@2" + }, + { + "command": "azureApiManagement.deleteAuthorizationAccessPolicy", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementAuthorizationAccessPolicy", + "group": "1@1" } ] }, diff --git a/package.nls.json b/package.nls.json index a623e6a..0ddb009 100644 --- a/package.nls.json +++ b/package.nls.json @@ -46,5 +46,17 @@ "azureApiManagement.revisions": "API Revisions", "azureApiManagement.setCustomHostName": "Select Gateway Host Name", "azureApiManagement.createSubscription": "Create a new Subscription", - "azureApiManagement.deleteSubscription": "Delete Subscription" + "azureApiManagement.deleteSubscription": "Delete Subscription", + "azureApiManagement.createAuthorizationProvider": "Create Authorization Provider", + "azureApiManagement.deleteAuthorizationProvider": "Delete Authorization Provider", + "azureApiManagement.copyAuthorizationProviderRedirectUrl": "Copy RedirectUrl", + "azureApiManagement.createAuthorization": "Create Authorization", + "azureApiManagement.authorizeAuthorization": "Login", + "azureApiManagement.deleteAuthorization": "Delete Authorization", + "azureApiManagement.copyAuthorizationPolicy": "Copy Policy Snippet", + "azureApiManagement.createAuthorizationAccessPolicy": "Create Access Policy", + "azureApiManagement.deleteAuthorizationAccessPolicy": "Delete Access Policy", + "azureApiManagement.showArmAuthorizationProvider": "Edit Authorization Provider", + "azureApiManagement.showArmAuthorization": "Edit Authorization", + "azureApiManagement.showArmAuthorizationAccessPolicy": "Edit Authorization Access Policy" } \ No newline at end of file diff --git a/resources/dark/accesspolicy.svg b/resources/dark/accesspolicy.svg new file mode 100644 index 0000000..52fd503 --- /dev/null +++ b/resources/dark/accesspolicy.svg @@ -0,0 +1 @@ +Icon-identity-233 \ No newline at end of file diff --git a/resources/dark/authorization.svg b/resources/dark/authorization.svg new file mode 100644 index 0000000..a1813b2 --- /dev/null +++ b/resources/dark/authorization.svg @@ -0,0 +1,7 @@ + + Icon-general-7 + + + + + \ No newline at end of file diff --git a/resources/dark/authorizationprovider.svg b/resources/dark/authorizationprovider.svg new file mode 100644 index 0000000..12cba68 --- /dev/null +++ b/resources/dark/authorizationprovider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/light/accesspolicy.svg b/resources/light/accesspolicy.svg new file mode 100644 index 0000000..52fd503 --- /dev/null +++ b/resources/light/accesspolicy.svg @@ -0,0 +1 @@ +Icon-identity-233 \ No newline at end of file diff --git a/resources/light/authorization.svg b/resources/light/authorization.svg new file mode 100644 index 0000000..a1813b2 --- /dev/null +++ b/resources/light/authorization.svg @@ -0,0 +1,7 @@ + + Icon-general-7 + + + + + \ No newline at end of file diff --git a/resources/light/authorizationprovider.svg b/resources/light/authorizationprovider.svg new file mode 100644 index 0000000..12cba68 --- /dev/null +++ b/resources/light/authorizationprovider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/azure/apim/ApimService.ts b/src/azure/apim/ApimService.ts index a7a136a..7967a8f 100644 --- a/src/azure/apim/ApimService.ts +++ b/src/azure/apim/ApimService.ts @@ -6,7 +6,7 @@ import { HttpOperationResponse, ServiceClient } from "@azure/ms-rest-js"; import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; import { createGenericClient } from "vscode-azureextensionui"; -import { IGatewayApiContract, IGatewayContract, IMasterSubscription } from "./contracts"; +import { IApimServiceContract, IAuthorizationAccessPolicyContract, IAuthorizationAccessPolicyPropertiesContract, IAuthorizationContract, IAuthorizationLoginLinkRequest, IAuthorizationLoginLinkResponse, IAuthorizationPropertiesContract, IAuthorizationProviderContract, IAuthorizationProviderPropertiesContract, IGatewayApiContract, IGatewayContract, IMasterSubscription, ITokenStoreIdentityProviderContract } from "./contracts"; export class ApimService { public baseUrl: string; @@ -16,6 +16,7 @@ export class ApimService { public resourceGroup: string; public serviceName: string; private readonly apiVersion: string = "2018-06-01-preview"; + private readonly authorizationProviderApiVersion: string = "2021-12-01-preview"; constructor(credentials: TokenCredentialsBase, endPointUrl: string, subscriptionId: string, resourceGroup: string, serviceName: string) { this.baseUrl = this.genSiteUrl(endPointUrl, subscriptionId, resourceGroup, serviceName); @@ -91,6 +92,191 @@ export class ApimService { return result.parsedBody; } + // Authorization Providers + public async listTokenStoreIdentityProviders(): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.baseUrl}/authorizationIdentityProviders?api-version=${this.authorizationProviderApiVersion}` + }); + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody.value); + } + + public async getTokenStoreIdentityProvider(providerName: string): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.baseUrl}/authorizationIdentityProviders/${providerName}?api-version=${this.authorizationProviderApiVersion}` + }); + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody); + } + + public async listAuthorizationProviders(): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.baseUrl}/authorizationProviders?api-version=${this.authorizationProviderApiVersion}` + }); + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody.value); + } + + public async listAuthorizations(authorizationProviderId: string): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderId}/authorizations?api-version=${this.authorizationProviderApiVersion}` + }); + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody.value); + } + + public async listAuthorizationAccessPolicies(authorizationProviderId: string, authorizationName: string): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderId}/authorizations/${authorizationName}/accesspolicies?api-version=${this.authorizationProviderApiVersion}` + }); + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody.value); + } + + public async getAuthorizationAccessPolicy(authorizationProviderId: string, authorizationName: string, accessPolicyName: string): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderId}/authorizations/${authorizationName}/accesspolicies/${accessPolicyName}?api-version=${this.authorizationProviderApiVersion}` + }); + + if (result.status === 404) { + return undefined; + } + + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody); + } + + public async createAuthorizationAccessPolicy(authorizationProviderId: string, authorizationName: string, accessPolicyName: string, accessPolicyPaylod: IAuthorizationAccessPolicyPropertiesContract): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "PUT", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderId}/authorizations/${authorizationName}/accesspolicies/${accessPolicyName}?api-version=${this.authorizationProviderApiVersion}`, + body: { properties: accessPolicyPaylod } + }); + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody); + } + + public async deleteAuthorizationAccessPolicy(authorizationProviderId: string, authorizationName: string, accessPolicyName: string): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + await client.sendRequest({ + method: "DELETE", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderId}/authorizations/${authorizationName}/accesspolicies/${accessPolicyName}?api-version=${this.authorizationProviderApiVersion}` + }); + } + + public async createAuthorizationProvider(authorizationProviderName: string, authorizationProviderPayload: IAuthorizationProviderPropertiesContract): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "PUT", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderName}?api-version=${this.authorizationProviderApiVersion}`, + body: { properties: authorizationProviderPayload } + }); + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody); + } + + public async createAuthorization(authorizationProviderName: string, authorizationName: string, authorizationPayload: IAuthorizationPropertiesContract): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "PUT", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderName}/authorizations/${authorizationName}?api-version=${this.authorizationProviderApiVersion}`, + body: { properties: authorizationPayload } + }); + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody); + } + + public async deleteAuthorization(authorizationProviderName: string, authorizationName: string): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + await client.sendRequest({ + method: "DELETE", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderName}/authorizations/${authorizationName}?api-version=${this.authorizationProviderApiVersion}` + }); + } + + public async getAuthorization(authorizationProviderName: string, authorizationName: string): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderName}/authorizations/${authorizationName}?api-version=${this.authorizationProviderApiVersion}` + }); + + if (result.status === 404) { + return undefined; + } + + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody); + } + + public async deleteAuthorizationProvider(authorizationProviderName: string): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + await client.sendRequest({ + method: "DELETE", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderName}?api-version=${this.authorizationProviderApiVersion}` + }); + } + + public async getAuthorizationProvider(authorizationProviderName: string): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderName}?api-version=${this.authorizationProviderApiVersion}` + }); + + if (result.status === 404) { + return undefined; + } + + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody); + } + + public async listAuthorizationLoginLinks(authorizationProviderName: string, authorizationName: string, loginLinkRequestPayload: IAuthorizationLoginLinkRequest): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "POST", + url: `${this.baseUrl}/authorizationProviders/${authorizationProviderName}/authorizations/${authorizationName}/getLoginLinks?api-version=${this.authorizationProviderApiVersion}`, + body: loginLinkRequestPayload + }); + // tslint:disable-next-line: no-unsafe-any + return (result.parsedBody); + } + + public async getService(): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.baseUrl}?api-version=${this.apiVersion}` + }); + // tslint:disable-next-line:no-any + return (result.parsedBody); + } + + public async turnOnManagedIdentity(): Promise { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "PATCH", + url: `${this.baseUrl}?api-version=${this.apiVersion}`, + body: { identity : { type: "systemassigned" } } + }); + // tslint:disable-next-line:no-any + return (result.parsedBody); + } + private genSiteUrl(endPointUrl: string, subscriptionId: string, resourceGroup: string, serviceName: string): string { return `${endPointUrl}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.ApiManagement/service/${serviceName}`; } diff --git a/src/azure/apim/contracts.ts b/src/azure/apim/contracts.ts index e11d623..419dd19 100644 --- a/src/azure/apim/contracts.ts +++ b/src/azure/apim/contracts.ts @@ -3,6 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +export interface IApimServiceContract { + id: string; + name: string; + // tslint:disable-next-line: no-reserved-keywords + type: string; + location?: string; + properties: object; + identity: IApimServiceIdentityContract; +} + +export interface IApimServiceIdentityContract { + // tslint:disable-next-line:no-reserved-keywords + type: string; + principalId: string; + tenantId: string; +} + export interface IGatewayContract { id: string; name: string; @@ -42,3 +59,129 @@ export interface ISubscriptionProperty { primaryKey: string; secondaryKey: string; } + +// Authorization Provider Contracts +export enum IGrantTypesContract { + authorizationCode = "authorizationCode", + clientCredentials = "clientCredentials" +} + +export interface IAuthorizationProviderContract { + id: string; + name: string; + // tslint:disable-next-line: no-reserved-keywords + type: string; + location?: string; + properties: IAuthorizationProviderPropertiesContract; +} + +export interface IAuthorizationProviderPropertiesContract { + displayName?: string; + identityProvider: string; + oauth2?: IAuthorizationProviderOAuth2SettingsContract; +} + +export interface IAuthorizationProviderOAuth2SettingsContract { + redirectUrl?: string; + grantTypes: IAuthorizationProviderOAuth2GrantTypesContract; +} + +export type IAuthorizationProviderOAuth2GrantTypesContract = { + [key in IGrantTypesContract]?: { + [key: string]: string | boolean + }; +}; + +export interface IAuthorizationContract { + id: string; + name: string; + // tslint:disable-next-line: no-reserved-keywords + type: string; + location?: string; + properties: IAuthorizationPropertiesContract; +} + +export interface IAuthorizationPropertiesContract { + authorizationType: string; + oauth2grantType: string; + parameters?: { + [key: string]: string | boolean; + }; + status?: ITokenStoreAuthorizationState; + error?: IAuthorizationErrorContract; +} + +export enum ITokenStoreAuthorizationState { + connected = "Connected", + error = "Error" +} + +export interface IAuthorizationErrorContract { + code: string; + message: string; + // tslint:disable-next-line:no-any + refreshResponseBodyFromIdentityProvider?: any; +} + +export interface ITokenStoreIdentityProviderContract { + id: string; + name: string; + // tslint:disable-next-line: no-reserved-keywords + type: string; + location?: string; + properties: ITokenStoreIdentityProviderPropertiesContract; +} + +export interface ITokenStoreIdentityProviderPropertiesContract { + displayName: string; + oauth2: { + grantTypes: ITokenStoreIdentityProviderGrantTypeContract; + }; +} + +export type ITokenStoreIdentityProviderGrantTypeContract = { + [key in IGrantTypesContract]?: ITokenStoreGrantTypeParameterContract; +}; + +export interface ITokenStoreGrantTypeParameterContract { + [key: string]: ITokenStoreGrantTypeParameterDefinitionContract; +} + +export interface ITokenStoreGrantTypeParameterDefinitionContract { + // tslint:disable-next-line:no-reserved-keywords + type: "string" | "securestring" | "bool"; + displayName: string; + description?: string; + // tslint:disable-next-line:no-reserved-keywords + default?: string; + uidefinition: { + atAuthorizationProviderLevel: "REQUIRED" | "OPTIONAL" | "HIDDEN" + }; +} + +export interface IAuthorizationLoginLinkRequest { + postLoginRedirectUrl: string; +} + +export interface IAuthorizationLoginLinkResponse { + loginLink: string; +} + +export interface IAuthorizationAccessPolicyContract { + id: string; + name: string; + // tslint:disable-next-line: no-reserved-keywords + type: string; + location?: string; + properties: IAuthorizationAccessPolicyPropertiesContract; +} + +export interface IAuthorizationAccessPolicyPropertiesContract { + objectId: string; + tenantId: string; +} + +export enum IAuthorizationTypeEnum { + OAuth2, + OAuth1 +} diff --git a/src/azure/graph/GraphService.ts b/src/azure/graph/GraphService.ts new file mode 100644 index 0000000..5461111 --- /dev/null +++ b/src/azure/graph/GraphService.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { HttpOperationResponse, ServiceClient } from "@azure/ms-rest-js"; +import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; +import { TokenResponse } from "adal-node"; +import { createGenericClient } from "vscode-azureextensionui"; +import { ext } from "../../extensionVariables"; +import { nonNullValue } from "../../utils/nonNull"; + +export class GraphService { + private accessToken: string; + constructor(private credentials: TokenCredentialsBase, + private graphEndpoint: string, + private tenantId: string) {} + + public async acquireGraphToken(): Promise { + const token = await this.credentials.getToken(); + this.credentials.authContext.acquireToken( + this.graphEndpoint, + nonNullValue(token.userId), + this.credentials.clientId, + (error, response) => { + if (error == null) { + this.accessToken = (response).accessToken; + } else { + ext.outputChannel.append(error.message); + } + } + ); + } + + // tslint:disable-next-line:no-any + public async getUser(emailId: string): Promise<{ userPrincipalName: string, objectId: string } | undefined> { + const client: ServiceClient = await createGenericClient(); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.graphEndpoint}/${this.tenantId}/users/${emailId}`, + headers: { + Authorization: `Bearer ${this.accessToken}`, + 'api-version': '1.61-internal' + } + }); + + if (result.status >= 400) { + // tslint:disable-next-line: no-any no-unsafe-any + ext.outputChannel.append(JSON.stringify(result.parsedBody)); + return undefined; + } + // tslint:disable-next-line:no-any + return <{ userPrincipalName: string, objectId: string }>(result.parsedBody); + } + + // tslint:disable-next-line:no-any + public async getGroup(displayNameOrEmail: string): Promise<{ displayName: string, objectId: string } | undefined> { + const client: ServiceClient = await createGenericClient(); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.graphEndpoint}/${this.tenantId}/groups?$filter=securityEnabled eq true and (startswith(displayName,'${displayNameOrEmail}') or startswith(mail,'${displayNameOrEmail}'))&$top=1`, + headers: { + Authorization: `Bearer ${this.accessToken}`, + 'api-version': '1.61-internal' + } + }); + + if (result.status >= 400) { + // tslint:disable-next-line: no-any no-unsafe-any + ext.outputChannel.append(JSON.stringify(result.parsedBody)); + return undefined; + } + + // tslint:disable-next-line: no-any no-unsafe-any + return <{ displayName: string, objectId: string }>(result.parsedBody.value[0]); + } + + // tslint:disable-next-line:no-any + public async getServicePrincipal(displayName: string): Promise<{ displayName: string, objectId: string } | undefined> { + const client: ServiceClient = await createGenericClient(); + const result: HttpOperationResponse = await client.sendRequest({ + method: "GET", + url: `${this.graphEndpoint}/${this.tenantId}/servicePrincipals?$filter=startswith(displayName,'${displayName}')&$top=1`, + headers: { + Authorization: `Bearer ${this.accessToken}`, + 'api-version': '1.61-internal' + } + }); + + if (result.status >= 400) { + // tslint:disable-next-line: no-any no-unsafe-any + ext.outputChannel.append(JSON.stringify(result.parsedBody)); + return undefined; + } + + // tslint:disable-next-line: no-any no-unsafe-any + return <{ displayName: string, objectId: string }>(result.parsedBody.value[0]); + } +} diff --git a/src/azure/resourceGraph/ResourceGraphService.ts b/src/azure/resourceGraph/ResourceGraphService.ts new file mode 100644 index 0000000..7be79d5 --- /dev/null +++ b/src/azure/resourceGraph/ResourceGraphService.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { HttpOperationResponse, ServiceClient } from "@azure/ms-rest-js"; +import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; +import { createGenericClient } from "vscode-azureextensionui"; + +export class ResourceGraphService { + public resourceGraphUrl: string; + constructor(public credentials: TokenCredentialsBase, + public endPointUrl: string, + public subscriptionId: string) { + this.credentials = credentials; + this.endPointUrl = endPointUrl; + this.subscriptionId = subscriptionId; + + this.resourceGraphUrl = `${this.endPointUrl}/providers/Microsoft.ResourceGraph/resources?api-version=2019-04-01`; + } + + // tslint:disable-next-line:no-any no-reserved-keywords + public async listSystemAssignedIdentities(): Promise<{ name: string, id: string, type: string, principalId: string }[]> { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "POST", + url: this.resourceGraphUrl, + body: { + subscriptions: [ this.subscriptionId ], + options: { resultFormat: "objectArray" }, + query: "Resources | where notempty(identity) | project name, id, type, principalId = identity.principalId" + }, + timeout: 5000 + }); + // tslint:disable-next-line:no-any no-unsafe-any no-reserved-keywords + return <{ name: string, id: string, type: string, principalId: string }[]>(result.parsedBody?.data); + } + + // tslint:disable-next-line:no-any + public async listUserAssignedIdentities(): Promise<{ name: string, id: string, principalId: string }[]> { + const client: ServiceClient = await createGenericClient(this.credentials); + const result: HttpOperationResponse = await client.sendRequest({ + method: "POST", + url: this.resourceGraphUrl, + body: { + subscriptions: [ this.subscriptionId ], + options: { resultFormat: "objectArray" }, + query: "resources | where type == 'microsoft.managedidentity/userassignedidentities' | project name, id, principalId = properties.principalId" + }, + timeout: 5000 + }); + // tslint:disable-next-line:no-any no-unsafe-any + return <{ name: string, id: string, principalId: string }[]>(result.parsedBody?.data); + } + +} diff --git a/src/commands/authorizations/authorizeAuthorization.ts b/src/commands/authorizations/authorizeAuthorization.ts new file mode 100644 index 0000000..7268e6e --- /dev/null +++ b/src/commands/authorizations/authorizeAuthorization.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { window } from 'vscode'; +import { IActionContext } from "vscode-azureextensionui"; +import { ApimService } from "../../azure/apim/ApimService"; +import { IAuthorizationProviderContract, ITokenStoreIdentityProviderContract } from "../../azure/apim/contracts"; +import { AuthorizationProviderTreeItem } from "../../explorer/AuthorizationProviderTreeItem"; +import { AuthorizationTreeItem } from "../../explorer/AuthorizationTreeItem"; +import { ext } from "../../extensionVariables"; +import { localize } from "../../localize"; +import { nonNullValue } from '../../utils/nonNull'; +import { askAuthorizationParameterValues } from './common'; + +export async function authorizeAuthorization(context: IActionContext, node?: AuthorizationTreeItem): Promise { + if (!node) { + const authorizationNode = await ext.tree.showTreeItemPicker(AuthorizationTreeItem.contextValue, context); + node = authorizationNode; + } + + const apimService = new ApimService( + node.root.credentials, + node.root.environment.resourceManagerEndpointUrl, + node.root.subscriptionId, + node.root.resourceGroupName, + node.root.serviceName); + + if (node.authorizationContract.properties.oauth2grantType === "AuthorizationCode") { + const extensionId = "ms-azuretools.vscode-apimanagement"; + const key = `vscodeauthcomplete/${node.root.authorizationProviderName}/${node.root.authorizationName}`; + const redirectUrl = `vscode://${extensionId}/${key}`; + const loginLinks = await apimService.listAuthorizationLoginLinks( + node.root.authorizationProviderName, + node.authorizationContract.name, + { postLoginRedirectUrl : redirectUrl }); + + vscode.env.openExternal(vscode.Uri.parse(loginLinks.loginLink)); + } else if (node.authorizationContract.properties.oauth2grantType === "ClientCredentials") { + const authorizationProvider : IAuthorizationProviderContract = (node.parent?.parent).authorizationProviderContract; + const identityProvider: ITokenStoreIdentityProviderContract = await apimService.getTokenStoreIdentityProvider(authorizationProvider.properties.identityProvider); + const grant = identityProvider.properties.oauth2.grantTypes.clientCredentials; + + const parameterValues = await askAuthorizationParameterValues(nonNullValue(grant)); + + const authorization = node.authorizationContract; + authorization.properties.parameters = parameterValues; + + window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: localize("authorizeAuthorization", `Updating Authorization '${authorization.name}' ...`), + cancellable: false + }, + // tslint:disable-next-line:no-non-null-assertion + async () => { return apimService.createAuthorization(authorizationProvider.name, authorization.name, authorization.properties); } + ).then(async () => { + // tslint:disable-next-line:no-non-null-assertion + await node!.refresh(context); + window.showInformationMessage(localize("updatedAuthorization", `Updated Authorization '${authorization.name}' succesfully.`)); + }); + } +} diff --git a/src/commands/authorizations/common.ts b/src/commands/authorizations/common.ts new file mode 100644 index 0000000..23a9bf4 --- /dev/null +++ b/src/commands/authorizations/common.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITokenStoreGrantTypeParameterContract, ITokenStoreGrantTypeParameterDefinitionContract } from "../../azure/apim/contracts"; +import { ext } from "../../extensionVariables"; +import { localize } from "../../localize"; + +export async function askAuthorizationProviderParameterValues(grant: ITokenStoreGrantTypeParameterContract) : Promise { + const parameterValues: IParameterValues = {}; + // tslint:disable-next-line:forin no-for-in + for (const parameter in grant) { + const parameterUIMetadata = grant[parameter]; + if (parameterUIMetadata.uidefinition.atAuthorizationProviderLevel !== "HIDDEN") { + parameterValues[parameter] = await askParam( + parameterUIMetadata, + parameterUIMetadata.uidefinition.atAuthorizationProviderLevel === "REQUIRED" ); + } + } + + return parameterValues; +} + +export async function askAuthorizationParameterValues(grant: ITokenStoreGrantTypeParameterContract) : Promise { + const parameterValues: IParameterValues = {}; + // tslint:disable-next-line:forin no-for-in + for (const parameter in grant) { + const parameterUIMetadata = grant[parameter]; + if (parameterUIMetadata.uidefinition.atAuthorizationProviderLevel === "HIDDEN") { + parameterValues[parameter] = await askParam( + parameterUIMetadata, + true); + } + } + + return parameterValues; +} + +async function askParam(parameterUIMetadata: ITokenStoreGrantTypeParameterDefinitionContract, isRequired: boolean) : Promise { + return await ext.ui.showInputBox({ + placeHolder: localize('parameterDisplayName', `Enter ${parameterUIMetadata.displayName} ...`), + prompt: localize('parameterDescription', `${parameterUIMetadata.description}`), + value: parameterUIMetadata.default, + password: parameterUIMetadata.type === "securestring", + validateInput: async (value: string | undefined): Promise => { + value = value ? value.trim() : ''; + + if (isRequired && value.length < 1) { + return localize("parameterRequired", `${parameterUIMetadata.displayName} is required.`); + } + + return undefined; + } + }); +} + +export async function askId(prompt: string, errorMessage: string, defaultValue: string = ''): Promise { + const idPrompt: string = localize('idPrompt', prompt); + return (await ext.ui.showInputBox({ + prompt: idPrompt, + value: defaultValue, + validateInput: async (value: string): Promise => { + value = value ? value.trim() : ''; + return validateId(value, errorMessage); + } + })).trim(); +} + +function validateId(id: string, errorMessage: string): string | undefined { + const test = "^[\w]+$)|(^[\w][\w\-]+[\w]$"; + if (id.match(test) === null) { + return localize("idInvalid", errorMessage); + } + + return undefined; +} + +export interface IParameterValues { + [key: string]: string; +} diff --git a/src/commands/authorizations/copyAuthorizationPolicy.ts b/src/commands/authorizations/copyAuthorizationPolicy.ts new file mode 100644 index 0000000..85550f7 --- /dev/null +++ b/src/commands/authorizations/copyAuthorizationPolicy.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { IActionContext } from "vscode-azureextensionui"; +import { AuthorizationTreeItem } from "../../explorer/AuthorizationTreeItem"; +import { ext } from "../../extensionVariables"; +import { localize } from '../../localize'; + +export async function copyAuthorizationPolicy(context: IActionContext, node?: AuthorizationTreeItem): Promise { + if (!node) { + const authorizationNode = await ext.tree.showTreeItemPicker(AuthorizationTreeItem.contextValue, context); + node = authorizationNode; + } + + // Select purpose + const attachToken = "Attach access token to backend request"; + const tokenBack = "Retrieve access token"; + const purposeOptions = [attachToken, tokenBack]; + const purposeSelected = await ext.ui.showQuickPick( + purposeOptions.map(purpose => { return { label: purpose, description: '', detail: '' }; }), + { placeHolder: 'How do you want to use the policy?', canPickMany: false }); + const managed = "managed"; + const jwt = "jwt"; + const identityTypeOptions = [ + { + label: managed, + description: "Use the managed identity of the service." + }, + { + label: jwt, + description: "Use the identity of the specified token." + } + ]; + const identityTypeSelected = await ext.ui.showQuickPick( + identityTypeOptions.map(option => { return { label: option.label, description: option.description, detail: '' }; }), + { placeHolder: 'Which identity type do you want to use?', canPickMany: false, suppressPersistence: true }); + + const pid = node.root.authorizationProviderName; + const aid = node.authorizationContract.name; + + let comment = ''; + let identityPhrase = ''; + let additionalMessage = ''; + if (identityTypeSelected.label === managed) { + comment = ``; + identityPhrase = `identity-type="${identityTypeSelected.label}"`; + additionalMessage = "For 'managed' identity-type, make sure managed identity is turned on."; + } else { + const allowedAudienceMessage = `Allowed audiences for jwt in "identity" attribute are "https://azure-api.net/authorization-manager"`; + comment = ``; + identityPhrase = `identity-type="${identityTypeSelected.label}" identity="@(context.Request.Headers["Authorization"][0].Replace("Bearer ", ""))"`; + additionalMessage = `For 'jwt' identity-type, ${allowedAudienceMessage}`; + } + + let policy = ''; + if (purposeSelected.label === attachToken) { + policy = `${comment} + + + @("Bearer " + ((Authorization)context.Variables.GetValueOrDefault("${pid}-${aid}-context"))?.AccessToken) +`; + } else { + policy = `${comment} + + + + @(((Authorization)context.Variables.GetValueOrDefault("${pid}-${aid}-context"))?.AccessToken) +`; + } + + vscode.env.clipboard.writeText(policy); + vscode.window.showInformationMessage(localize("CopySnippet", `Policy copied to clipboard. ${additionalMessage}`)); + ext.outputChannel.appendLine(`Policy copied to clipboard. ${additionalMessage}`); +} diff --git a/src/commands/authorizations/copyAuthorizationProviderRedirectUrl.ts b/src/commands/authorizations/copyAuthorizationProviderRedirectUrl.ts new file mode 100644 index 0000000..482fed2 --- /dev/null +++ b/src/commands/authorizations/copyAuthorizationProviderRedirectUrl.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { IActionContext } from "vscode-azureextensionui"; +import { AuthorizationProviderTreeItem } from '../../explorer/AuthorizationProviderTreeItem'; +import { ext } from "../../extensionVariables"; +import { localize } from '../../localize'; +import { nonNullValue } from '../../utils/nonNull'; + +export async function copyAuthorizationProviderRedirectUrl(context: IActionContext, node?: AuthorizationProviderTreeItem): Promise { + if (!node) { + node = await ext.tree.showTreeItemPicker(AuthorizationProviderTreeItem.contextValue, context); + } + + const redirectUrl = nonNullValue(node.authorizationProviderContract.properties.oauth2?.redirectUrl); + vscode.env.clipboard.writeText(redirectUrl); + vscode.window.showInformationMessage(localize("copyRedirect", `RedirectUrl for Authorization provider '${node.authorizationProviderContract.name}' copied to clipboard. value - ${redirectUrl}`)); + ext.outputChannel.appendLine(`RedirectUrl for Authorization provider '${node.authorizationProviderContract.name}' copied to clipboard. value - ${redirectUrl}`); +} diff --git a/src/commands/authorizations/createAuthorization.ts b/src/commands/authorizations/createAuthorization.ts new file mode 100644 index 0000000..7ba62c4 --- /dev/null +++ b/src/commands/authorizations/createAuthorization.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from "vscode-azureextensionui"; +import { AuthorizationProviderTreeItem } from "../../explorer/AuthorizationProviderTreeItem"; +import { AuthorizationsTreeItem, IAuthorizationTreeItemContext } from "../../explorer/AuthorizationsTreeItem"; +import { ext } from "../../extensionVariables"; + +export async function createAuthorization(context: IActionContext & Partial, node?: AuthorizationsTreeItem): Promise { + if (!node) { + const authorizationProviderNode = await ext.tree.showTreeItemPicker(AuthorizationProviderTreeItem.contextValue, context); + node = authorizationProviderNode.authorizationsTreeItem; + } + + await node.createChild(context); +} diff --git a/src/commands/authorizations/createAuthorizationAccessPolicy.ts b/src/commands/authorizations/createAuthorizationAccessPolicy.ts new file mode 100644 index 0000000..0c465d7 --- /dev/null +++ b/src/commands/authorizations/createAuthorizationAccessPolicy.ts @@ -0,0 +1,227 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; +import { ProgressLocation, QuickPickItem, window } from "vscode"; +import { IActionContext } from "vscode-azureextensionui"; +import { ApimService } from "../../azure/apim/ApimService"; +import { GraphService } from "../../azure/graph/GraphService"; +import { ResourceGraphService } from "../../azure/resourceGraph/ResourceGraphService"; +import { AuthorizationAccessPoliciesTreeItem, IAuthorizationAccessPolicyTreeItemContext } from "../../explorer/AuthorizationAccessPoliciesTreeItem"; +import { AuthorizationTreeItem } from "../../explorer/AuthorizationTreeItem"; +import { ext } from "../../extensionVariables"; +import { localize } from "../../localize"; +import { nonNullValue } from "../../utils/nonNull"; + +const systemAssignedManagedIdentitiesOptionLabel = "System assigned managed identity"; +const userAssignedManagedIdentitiesOptionLabel = "User assigned managed identity"; +const userEmailIdLabel = "User"; +const groupDisplayNameorEmailIdLabel = "Group"; +const servicePrincipalDisplayNameLabel = "Service principal"; + +let resourceGraphService: ResourceGraphService; +let graphService: GraphService; + + // tslint:disable-next-line: no-any no-unsafe-any +export async function createAuthorizationAccessPolicy(context: IActionContext & Partial, node?: AuthorizationAccessPoliciesTreeItem): Promise { + if (!node) { + const AuthorizationNode = await ext.tree.showTreeItemPicker(AuthorizationTreeItem.contextValue, context); + node = AuthorizationNode.authorizationAccessPoliciesTreeItem; + } + + const apimService = new ApimService( + node.root.credentials, + node.root.environment.resourceManagerEndpointUrl, + node.root.subscriptionId, + node.root.resourceGroupName, + node.root.serviceName); + + resourceGraphService = new ResourceGraphService( + node.root.credentials, + node.root.environment.resourceManagerEndpointUrl, + node.root.subscriptionId + ); + + graphService = new GraphService( + node.root.credentials, + nonNullValue(node.root.environment.activeDirectoryGraphResourceId), + node.root.tenantId + ); + + await graphService.acquireGraphToken(); + + const identityOptions = await populateIdentityOptionsAsync( + apimService, node.root.credentials, node.root.environment.resourceManagerEndpointUrl); + + const identitySelected = await ext.ui.showQuickPick( + identityOptions, { placeHolder: 'Select identity...', canPickMany: false, suppressPersistence: true }); + + let permissionName = ''; + let oid = ''; + + if (identitySelected.label === systemAssignedManagedIdentitiesOptionLabel) { + const response = await resourceGraphService.listSystemAssignedIdentities(); + // tslint:disable-next-line: no-any no-unsafe-any + const otherManagedIdentityOptions = await populateManageIdentityOptions(response); + + const managedIdentitySelected = await ext.ui.showQuickPick( + otherManagedIdentityOptions, { placeHolder: 'Select system assigned managed identity ...', canPickMany: false, suppressPersistence: true }); + + permissionName = managedIdentitySelected.label; + oid = nonNullValue(managedIdentitySelected.description); + } else if (identitySelected.label === userAssignedManagedIdentitiesOptionLabel) { + const response = await resourceGraphService.listUserAssignedIdentities(); + const otherManagedIdentityOptions = await populateManageIdentityOptions(response); + + const managedIdentitySelected = await ext.ui.showQuickPick( + otherManagedIdentityOptions, { placeHolder: 'Select user assigned managed identity ...', canPickMany: false, suppressPersistence: true }); + + permissionName = managedIdentitySelected.label; + oid = nonNullValue(managedIdentitySelected.description); + } else if (identitySelected.label === userEmailIdLabel) { + const userId = await askInput('Enter user emailId ...', 'mary@contoso.net'); + const user = await graphService.getUser(userId); + + if (user !== undefined && user.objectId !== null) { + permissionName = user.userPrincipalName; + oid = user.objectId; + } else { + window.showErrorMessage(localize('invalidUserEmailId', 'Please specify a valid user emailId.')); + } + } else if (identitySelected.label === groupDisplayNameorEmailIdLabel) { + const groupDisplayNameOrEmailId = await askInput('Enter group displayname (or) emailId ...', 'myfullgroupname (or) mygroup@contoso.net'); + const group = await graphService.getGroup(groupDisplayNameOrEmailId); + + if (group !== undefined && group.objectId !== null) { + permissionName = group.displayName.replace(' ', ''); + oid = group.objectId; + } else { + window.showErrorMessage(localize('invalidGroupDisplayNameorEmailId', 'Please specify a valid group display name (or) emailId. Example, myfullgroupname (or) mygroup@contoso.net')); + } + } else if (identitySelected.label === servicePrincipalDisplayNameLabel) { + const servicePrincipalDisplayName = await askInput('Enter service principal display name ...', 'myserviceprincipalname'); + + const spn = await graphService.getServicePrincipal(servicePrincipalDisplayName); + + if (spn !== undefined && spn.objectId !== null) { + permissionName = spn.displayName.replace(' ', ''); + oid = spn.objectId; + } else { + window.showErrorMessage(localize('invalidSpnDisplayName', 'Please specify a valid service principal display name.')); + } + } else { + permissionName = identitySelected.label; + oid = nonNullValue(identitySelected.description); + } + + context.authorizationAccessPolicyName = permissionName; + context.authorizationAccessPolicy = { + objectId: oid, + tenantId: node.root.tenantId + }; + + createAccessPolicy(permissionName, node, context); +} + +function createAccessPolicy( + permissionName: string, + node: AuthorizationAccessPoliciesTreeItem, + context: IActionContext & Partial) : void { + window.withProgress( + { + location: ProgressLocation.Notification, + title: localize("creatingAuthorizationPermission", `Creating Access policy '${permissionName}' for Authorization ${node.root.authorizationName} ...`), + cancellable: false + }, + async () => { + // tslint:disable-next-line:no-non-null-assertion + return node!.createChild(context); + } + ).then(async () => { + // tslint:disable-next-line:no-non-null-assertion + await node!.refresh(context); + window.showInformationMessage(localize("createdAuthorizationPermission", `Created Access policy '${permissionName}' successfully.`)); + }); +} + +async function populateIdentityOptionsAsync( + apimService: ApimService, + credential : TokenCredentialsBase, + resourceManagerEndpointUrl: string) : Promise { + const options : QuickPickItem[] = []; + + // 1. Self + const token = await credential.getToken(); + const meOption : QuickPickItem = { + label: nonNullValue(token.userId), + description: token.oid, + detail: "Current signedIn user" + }; + options.push(meOption); + + // 2. APIM Service + const service = await apimService.getService(); + if (!!service.identity?.principalId) { + const apimOption : QuickPickItem = { + label: service.name, + description: service.identity.principalId, + detail: "Current service system managed identity" + }; + options.push(apimOption); + } + + // 3. Other Managed identities. Dogfood doesn't support this endpoint, so only show this in prod + if (resourceManagerEndpointUrl === "https://management.azure.com/") { + const systemAssignedManagedIdentities : QuickPickItem = { + label: systemAssignedManagedIdentitiesOptionLabel, + description: "", + detail: "" + }; + options.push(systemAssignedManagedIdentities); + + const userAssignedManagedIdentities : QuickPickItem = { + label: userAssignedManagedIdentitiesOptionLabel, + description: "", + detail: "" + }; + options.push(userAssignedManagedIdentities); + } + + // 4. Custom + options.push({ label: userEmailIdLabel }); + options.push({ label: groupDisplayNameorEmailIdLabel }); + options.push({ label: servicePrincipalDisplayNameLabel }); + return options; +} + +// tslint:disable-next-line:no-any +async function populateManageIdentityOptions(data: { name: string, id: string, principalId: string }[]) : Promise { + const options : QuickPickItem[] = []; + const managedIdentityOptions : QuickPickItem[] = data.map(d => { + return { + label: d.name, + description: d.principalId, + detail: d.id + }; + }); + options.push(...managedIdentityOptions); + + return options; +} + +async function askInput(message: string, placeholder: string = '') : Promise { + const idPrompt: string = localize('value', message); + return (await ext.ui.showInputBox({ + prompt: idPrompt, + placeHolder: placeholder, + validateInput: async (value: string): Promise => { + value = value ? value.trim() : ''; + if (value === '') { + return localize("valueInvalid", 'Value cannot be empty.'); + } + return undefined; + } + })).trim(); +} diff --git a/src/commands/authorizations/createAuthorizationProvider.ts b/src/commands/authorizations/createAuthorizationProvider.ts new file mode 100644 index 0000000..b26f662 --- /dev/null +++ b/src/commands/authorizations/createAuthorizationProvider.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from "vscode-azureextensionui"; +import { AuthorizationProvidersTreeItem, IAuthorizationProviderTreeItemContext } from "../../explorer/AuthorizationProvidersTreeItem"; +import { ServiceTreeItem } from "../../explorer/ServiceTreeItem"; +import { ext } from "../../extensionVariables"; + +export async function createAuthorizationProvider(context: IActionContext & Partial, node?: AuthorizationProvidersTreeItem): Promise { + if (!node) { + const serviceNode = await ext.tree.showTreeItemPicker(ServiceTreeItem.contextValue, context); + node = serviceNode.authorizationProvidersTreeItem; + } + + // support update + await node.createChild(context); +} diff --git a/src/explorer/AuthorizationAccessPoliciesTreeItem.ts b/src/explorer/AuthorizationAccessPoliciesTreeItem.ts new file mode 100644 index 0000000..4472c12 --- /dev/null +++ b/src/explorer/AuthorizationAccessPoliciesTreeItem.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzExtTreeItem, AzureParentTreeItem, ICreateChildImplContext } from "vscode-azureextensionui"; +import { ApimService } from "../azure/apim/ApimService"; +import { IAuthorizationAccessPolicyContract, IAuthorizationAccessPolicyPropertiesContract } from "../azure/apim/contracts"; +import { localize } from "../localize"; +import { processError } from "../utils/errorUtil"; +import { treeUtils } from "../utils/treeUtils"; +import { AuthorizationAccessPolicyTreeItem } from "./AuthorizationAccessPolicyTreeItem"; +import { IAuthorizationTreeRoot } from "./IAuthorizationTreeRoot"; + +export interface IAuthorizationAccessPolicyTreeItemContext extends ICreateChildImplContext { + authorizationAccessPolicyName: string; + authorizationAccessPolicy: IAuthorizationAccessPolicyPropertiesContract; +} + +export class AuthorizationAccessPoliciesTreeItem extends AzureParentTreeItem { + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('list'); + } + public static contextValue: string = 'azureApiManagementAuthorizationAccessPolicies'; + public label: string = "Access policies"; + public contextValue: string = AuthorizationAccessPoliciesTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.AuthorizationAccessPolicy', 'AuthorizationAccessPolicy'); + private _nextLink: string | undefined; + + public hasMoreChildrenImpl(): boolean { + return this._nextLink !== undefined; + } + + public async loadMoreChildrenImpl(clearCache: boolean): Promise { + if (clearCache) { + this._nextLink = undefined; + } + + const apimService = new ApimService( + this.root.credentials, + this.root.environment.resourceManagerEndpointUrl, + this.root.subscriptionId, + this.root.resourceGroupName, + this.root.serviceName); + + const authorizationAccessPolicies: IAuthorizationAccessPolicyContract[] = await apimService.listAuthorizationAccessPolicies( + this.root.authorizationProviderName, + this.root.authorizationName); + + return this.createTreeItemsWithErrorHandling( + authorizationAccessPolicies, + "invalidApiManagementAuthorizationAccessPolicy", + async (accessPolicy: IAuthorizationAccessPolicyContract) => new AuthorizationAccessPolicyTreeItem(this, accessPolicy), + (accessPolicy: IAuthorizationAccessPolicyContract) => { + return accessPolicy.name; + }); + } + + public async createChildImpl(context: IAuthorizationAccessPolicyTreeItemContext): Promise { + if (context.authorizationAccessPolicyName + && context.authorizationAccessPolicy !== undefined) { + const authorizationAccessPolicyName = context.authorizationAccessPolicyName; + context.showCreatingTreeItem(authorizationAccessPolicyName); + + try { + const apimService = new ApimService(this.root.credentials, this.root.environment.resourceManagerEndpointUrl, this.root.subscriptionId, this.root.resourceGroupName, this.root.serviceName); + let authorizationAccessPolicy = await apimService.getAuthorizationAccessPolicy(this.root.authorizationProviderName, this.root.authorizationName, authorizationAccessPolicyName); + if (authorizationAccessPolicy === undefined) { + authorizationAccessPolicy = await apimService.createAuthorizationAccessPolicy( + this.root.authorizationProviderName, + this.root.authorizationName, + authorizationAccessPolicyName, + context.authorizationAccessPolicy); + return new AuthorizationAccessPolicyTreeItem(this, authorizationAccessPolicy); + } else { + throw new Error(localize("createAuthorizationAccessPolicy", `Access policy '${authorizationAccessPolicyName}' already exists.`)); + } + } catch (error) { + throw new Error(processError(error, localize("createAuthorizationAccessPolicy", `Failed to access policy '${authorizationAccessPolicyName}' to Authorization '${this.root.authorizationName}'.`))); + } + } else { + throw Error("Expected Access Policy name."); + } + } +} diff --git a/src/explorer/AuthorizationAccessPolicyTreeItem.ts b/src/explorer/AuthorizationAccessPolicyTreeItem.ts new file mode 100644 index 0000000..00cdb5c --- /dev/null +++ b/src/explorer/AuthorizationAccessPolicyTreeItem.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProgressLocation, window } from "vscode"; +import { AzureParentTreeItem, AzureTreeItem, DialogResponses, ISubscriptionContext, UserCancelledError } from "vscode-azureextensionui"; +import { ApimService } from "../azure/apim/ApimService"; +import { IAuthorizationAccessPolicyContract } from "../azure/apim/contracts"; +import { localize } from "../localize"; +import { nonNullProp } from "../utils/nonNull"; +import { treeUtils } from "../utils/treeUtils"; +import { IAuthorizationAccessPolicyTreeRoot } from "./IAuthorizationAccessPolicyTreeRoot"; + +export class AuthorizationAccessPolicyTreeItem extends AzureTreeItem { + public static contextValue: string = 'azureApiManagementAuthorizationAccessPolicy'; + public contextValue: string = AuthorizationAccessPolicyTreeItem.contextValue; + public readonly commandId: string = 'azureApiManagement.showArmAuthorizationAccessPolicy'; + + private _label: string; + private _root: IAuthorizationAccessPolicyTreeRoot; + + constructor( + parent: AzureParentTreeItem, + public readonly authorizationAccessPolicyContract: IAuthorizationAccessPolicyContract) { + super(parent); + + this._root = this.createRoot(parent.root); + + this._label = nonNullProp(authorizationAccessPolicyContract, 'name'); + } + + public get label() : string { + return this._label; + } + + public get root(): IAuthorizationAccessPolicyTreeRoot { + return this._root; + } + + public get description(): string | undefined { + return this.authorizationAccessPolicyContract.properties.objectId; + } + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('accesspolicy'); + } + + public async deleteTreeItemImpl(): Promise { + const message: string = localize("confirmAccessPolicyRemove", `Are you sure you want to remove Access Policy '${this.authorizationAccessPolicyContract.name}' from Authorization '${this.root.authorizationName}'?`); + const result = await window.showWarningMessage(message, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel); + if (result === DialogResponses.deleteResponse) { + const deletingMessage: string = localize("removingAuthorizationAccessPolicy", `Removing Access Policy "${this.authorizationAccessPolicyContract.name}" from Authorization '${this.root.authorizationName}.'`); + await window.withProgress({ location: ProgressLocation.Notification, title: deletingMessage }, async () => { + const apimService = new ApimService(this.root.credentials, this.root.environment.resourceManagerEndpointUrl, this.root.subscriptionId, this.root.resourceGroupName, this.root.serviceName); + await apimService.deleteAuthorizationAccessPolicy( + this.root.authorizationProviderName, + this.root.authorizationName, + nonNullProp(this.authorizationAccessPolicyContract, "name")); + }); + // don't wait + window.showInformationMessage(localize("removedAuthorizationAccessPolicy", `Successfully removed Access Policy "${this.authorizationAccessPolicyContract.name}" from Authorization '${this.root.authorizationName}'.`)); + + } else { + throw new UserCancelledError(); + } + } + + private createRoot(subRoot: ISubscriptionContext): IAuthorizationAccessPolicyTreeRoot { + return Object.assign({}, subRoot, { + accessPolicyName: nonNullProp(this.authorizationAccessPolicyContract, 'name') + }); + } +} diff --git a/src/explorer/AuthorizationProviderTreeItem.ts b/src/explorer/AuthorizationProviderTreeItem.ts new file mode 100644 index 0000000..4f8086f --- /dev/null +++ b/src/explorer/AuthorizationProviderTreeItem.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProgressLocation, window } from "vscode"; +import { AzureParentTreeItem, AzureTreeItem, DialogResponses, ISubscriptionContext, UserCancelledError } from "vscode-azureextensionui"; +import { ApimService } from "../azure/apim/ApimService"; +import { IAuthorizationProviderContract } from "../azure/apim/contracts"; +import { localize } from "../localize"; +import { nonNullProp } from "../utils/nonNull"; +import { treeUtils } from "../utils/treeUtils"; +import { AuthorizationsTreeItem } from "./AuthorizationsTreeItem"; +import { AuthorizationTreeItem } from "./AuthorizationTreeItem"; +import { IAuthorizationProviderTreeRoot } from "./IAuthorizationProviderTreeRoot"; +import { IServiceTreeRoot } from "./IServiceTreeRoot"; + +export class AuthorizationProviderTreeItem extends AzureParentTreeItem { + public static contextValue: string = 'azureApiManagementAuthorizationProvider'; + public contextValue: string = AuthorizationProviderTreeItem.contextValue; + public readonly authorizationsTreeItem: AuthorizationsTreeItem; + public readonly commandId: string = 'azureApiManagement.showArmAuthorizationProvider'; + + private _label: string; + private _root: IAuthorizationProviderTreeRoot; + + constructor( + parent: AzureParentTreeItem, + public readonly authorizationProviderContract: IAuthorizationProviderContract) { + super(parent); + this._label = nonNullProp(this.authorizationProviderContract, 'name'); + this._root = this.createRoot(parent.root); + + this.authorizationsTreeItem = new AuthorizationsTreeItem(this); + } + + public get label() : string { + return this._label; + } + + public get root(): IAuthorizationProviderTreeRoot { + return this._root; + } + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('authorizationprovider'); + } + + public hasMoreChildrenImpl(): boolean { + return false; + } + + public async loadMoreChildrenImpl(): Promise[]> { + return [this.authorizationsTreeItem]; + } + + public pickTreeItemImpl(expectedContextValues: (string | RegExp)[]): AzureTreeItem | undefined { + for (const expectedContextValue of expectedContextValues) { + switch (expectedContextValue) { + case AuthorizationTreeItem.contextValue: + return this.authorizationsTreeItem; + default: + } + } + return undefined; + } + + public async deleteTreeItemImpl(): Promise { + const message: string = localize("confirmDeleteAuthorizationProvider", `Are you sure you want to remove Authorization provider '${this.authorizationProviderContract.name}'?`); + const result = await window.showWarningMessage(message, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel); + if (result === DialogResponses.deleteResponse) { + const deletingMessage: string = localize("removingAuthorizationProvider", `Removing Authorization provider "${this.authorizationProviderContract.name}".'`); + await window.withProgress({ location: ProgressLocation.Notification, title: deletingMessage }, async () => { + const apimService = new ApimService(this.root.credentials, this.root.environment.resourceManagerEndpointUrl, this.root.subscriptionId, this.root.resourceGroupName, this.root.serviceName); + await apimService.deleteAuthorizationProvider(this.root.authorizationProviderName); + }); + // don't wait + window.showInformationMessage(localize("removedAuthorizationProvider", `Successfully removed Authorization provider "${this.authorizationProviderContract.name}".`)); + + } else { + throw new UserCancelledError(); + } + } + + private createRoot(subRoot: ISubscriptionContext): IAuthorizationProviderTreeRoot { + return Object.assign({}, subRoot, { + authorizationProviderName: nonNullProp(this.authorizationProviderContract, 'name') + }); + } +} diff --git a/src/explorer/AuthorizationProvidersTreeItem.ts b/src/explorer/AuthorizationProvidersTreeItem.ts new file mode 100644 index 0000000..0ff6baa --- /dev/null +++ b/src/explorer/AuthorizationProvidersTreeItem.ts @@ -0,0 +1,171 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProgressLocation, window } from "vscode"; +import { AzExtTreeItem, AzureParentTreeItem, ICreateChildImplContext } from "vscode-azureextensionui"; +import { ApimService } from "../azure/apim/ApimService"; +import { IAuthorizationProviderContract, IAuthorizationProviderOAuth2GrantTypesContract, IAuthorizationProviderPropertiesContract, IGrantTypesContract, ITokenStoreGrantTypeParameterContract, ITokenStoreIdentityProviderContract } from "../azure/apim/contracts"; +import { askAuthorizationProviderParameterValues, askId } from "../commands/authorizations/common"; +import { ext } from "../extensionVariables"; +import { localize } from "../localize"; +import { processError } from "../utils/errorUtil"; +import { treeUtils } from "../utils/treeUtils"; +import { AuthorizationProviderTreeItem } from "./AuthorizationProviderTreeItem"; +import { IServiceTreeRoot } from "./IServiceTreeRoot"; + +export interface IAuthorizationProviderTreeItemContext extends ICreateChildImplContext { + name: string; + authorizationProvider: IAuthorizationProviderPropertiesContract; +} + +export class AuthorizationProvidersTreeItem extends AzureParentTreeItem { + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('list'); + } + public static contextValue: string = 'azureApiManagementAuthorizationProviders'; + public readonly childTypeLabel: string = localize('azureApiManagement.AuthorizationProvider', 'AuthorizationProvider'); + public label: string = "Authorizations (preview)"; + public contextValue: string = AuthorizationProvidersTreeItem.contextValue; + private _nextLink: string | undefined; + private apimService: ApimService; + + public hasMoreChildrenImpl(): boolean { + return this._nextLink !== undefined; + } + + public async loadMoreChildrenImpl(clearCache: boolean): Promise { + if (clearCache) { + this._nextLink = undefined; + } + + this.apimService = new ApimService( + this.root.credentials, + this.root.environment.resourceManagerEndpointUrl, + this.root.subscriptionId, + this.root.resourceGroupName, + this.root.serviceName); + + const tokenProviders: IAuthorizationProviderContract[] = await this.apimService.listAuthorizationProviders(); + + return this.createTreeItemsWithErrorHandling( + tokenProviders, + "invalidApiManagementAuthorizationProvider", + async (authorizationProvider: IAuthorizationProviderContract) => new AuthorizationProviderTreeItem(this, authorizationProvider), + (authorizationProvider: IAuthorizationProviderContract) => { + return authorizationProvider.name; + }); + } + + public async createChildImpl(context: IAuthorizationProviderTreeItemContext): Promise { + await this.checkManagedIdentityEnabled(); + await this.buildContext(context); + if (context.name !== null && context.authorizationProvider !== null) { + return window.withProgress( + { + location: ProgressLocation.Notification, + title: localize("creatingAuthorizationProvider", `Creating Authorization provider '${context.name}' in service ${this.root.serviceName} ...`), + cancellable: false + }, + // tslint:disable-next-line:no-non-null-assertion + async (): Promise => { + const authorizationProviderName = context.name; + context.showCreatingTreeItem(authorizationProviderName); + try { + let authorizationProvider = await this.apimService.getAuthorizationProvider(context.name); + if (authorizationProvider === undefined) { + authorizationProvider = await this.apimService.createAuthorizationProvider(context.name, context.authorizationProvider); + const message = `Please add redirect url '${authorizationProvider.properties.oauth2?.redirectUrl}' to the OAuth application.`; + ext.outputChannel.show(); + ext.outputChannel.appendLine(message); + window.showWarningMessage(localize("redirectUrlMessage", message)); + window.showInformationMessage(localize("createdAuthorizationProvider", `Created Authorization provider '${context.name}'.`)); + return new AuthorizationProviderTreeItem(this, authorizationProvider); + } else { + throw new Error(localize("createAuthorizationProvider", `Authorization provider '${authorizationProviderName}' already exists.`)); + } + } catch (error) { + throw new Error(processError(error, localize("createAuthorizationProvider", `Failed to create Authorization provider '${authorizationProviderName}'.`))); + } + } + ); + } else { + throw Error("Expected Authorization provider information."); + } + } + + private async checkManagedIdentityEnabled() : Promise { + const service = await this.apimService.getService(); + if (service.identity === undefined) { + const options = ['Yes', 'No']; + const option = await ext.ui.showQuickPick(options.map((s) => { return { label: s, description: '', detail: '' }; }), { placeHolder: 'Enable system assigned managed identity', canPickMany: false }); + if (option.label === options[0]) { + await window.withProgress( + { + location: ProgressLocation.Notification, + title: localize("enableManagedIdentity", `Enabling system assigned managed identity.`), + cancellable: false + }, + async () => { + await this.apimService.turnOnManagedIdentity(); + window.showInformationMessage(localize("enabledManagedIdentity", `Enabled system assigned managed identity.`)); + } + ); + } + } + } + + private async buildContext(context: IAuthorizationProviderTreeItemContext): Promise { + let supportedIdentityProviders: ITokenStoreIdentityProviderContract[] = await this.apimService.listTokenStoreIdentityProviders(); + // tslint:disable-next-line:no-function-expression + supportedIdentityProviders = supportedIdentityProviders.sort(function compare(a: ITokenStoreIdentityProviderContract, b: ITokenStoreIdentityProviderContract): number { + return a.properties.displayName.localeCompare(b.properties.displayName); + }); + + const identityProviderPicked = await ext.ui.showQuickPick(supportedIdentityProviders.map((s) => { return { label: s.properties.displayName, description: '', detail: '' }; }), { placeHolder: 'Select identity provider ...', canPickMany: false }); + const selectedIdentityProvider = supportedIdentityProviders.find(s => s.properties.displayName === identityProviderPicked.label); + + let grantType: string = ""; + if (selectedIdentityProvider + && selectedIdentityProvider.properties.oauth2.grantTypes !== null) { + const authorizationProviderName = await askId( + 'Enter Authorization provider name ...', + 'Invalid Authorization provider name.'); + + const grantTypes = Object.keys(selectedIdentityProvider.properties.oauth2.grantTypes); + if (grantTypes.length > 1) { + const grantTypePicked = await ext.ui.showQuickPick(grantTypes.map((s) => { return { label: s[0].toUpperCase() + s.slice(1), description: '', detail: '' }; }), { placeHolder: 'Select grant type ...', canPickMany: false }); + grantType = grantTypePicked.label[0].toLocaleLowerCase() + grantTypePicked.label.slice(1); + } else { + grantType = grantTypes[0]; + } + + const grantTypeValue: IGrantTypesContract = grantType; + + // tslint:disable-next-line:no-any + // tslint:disable-next-line:no-unsafe-any + const grant: ITokenStoreGrantTypeParameterContract = selectedIdentityProvider?.properties.oauth2.grantTypes[grantType]; + + const parameterValues = await askAuthorizationProviderParameterValues(grant); + + const authorizationProviderGrant: IAuthorizationProviderOAuth2GrantTypesContract = {}; + if (grantTypeValue === IGrantTypesContract.authorizationCode) { + authorizationProviderGrant.authorizationCode = parameterValues; + } else if (grantTypeValue === IGrantTypesContract.clientCredentials) { + authorizationProviderGrant.clientCredentials = parameterValues; + } + + const authorizationProviderPayload: IAuthorizationProviderPropertiesContract = { + identityProvider: selectedIdentityProvider.name, + oauth2: { + grantTypes: authorizationProviderGrant + } + }; + + context.name = authorizationProviderName; + context.authorizationProvider = authorizationProviderPayload; + } + } +} diff --git a/src/explorer/AuthorizationTreeItem.ts b/src/explorer/AuthorizationTreeItem.ts new file mode 100644 index 0000000..580771e --- /dev/null +++ b/src/explorer/AuthorizationTreeItem.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProgressLocation, window } from "vscode"; +import { AzureParentTreeItem, AzureTreeItem, DialogResponses, ISubscriptionContext, UserCancelledError } from "vscode-azureextensionui"; +import { ApimService } from "../azure/apim/ApimService"; +import { IAuthorizationContract } from "../azure/apim/contracts"; +import { localize } from "../localize"; +import { nonNullProp } from "../utils/nonNull"; +import { treeUtils } from "../utils/treeUtils"; +import { AuthorizationAccessPoliciesTreeItem } from "./AuthorizationAccessPoliciesTreeItem"; +import { AuthorizationAccessPolicyTreeItem } from "./AuthorizationAccessPolicyTreeItem"; +import { IAuthorizationProviderTreeRoot } from "./IAuthorizationProviderTreeRoot"; +import { IAuthorizationTreeRoot } from "./IAuthorizationTreeRoot"; + +export class AuthorizationTreeItem extends AzureParentTreeItem { + public static contextValue: string = 'azureApiManagementAuthorization'; + public readonly authorizationAccessPoliciesTreeItem: AuthorizationAccessPoliciesTreeItem; + public contextValue: string = AuthorizationTreeItem.contextValue; + public readonly commandId: string = 'azureApiManagement.showArmAuthorization'; + private _label: string; + private _root: IAuthorizationTreeRoot; + + constructor( + parent: AzureParentTreeItem, + public authorizationContract: IAuthorizationContract) { + super(parent); + this._label = nonNullProp(authorizationContract, 'name'); + + this._root = this.createRoot(parent.root); + + this.authorizationAccessPoliciesTreeItem = new AuthorizationAccessPoliciesTreeItem(this); + } + + public get label(): string { + return this._label; + } + + public get description(): string | undefined { + return this.authorizationContract.properties.status; + } + + public get root(): IAuthorizationTreeRoot { + return this._root; + } + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('authorization'); + } + + public async loadMoreChildrenImpl(): Promise[]> { + return [this.authorizationAccessPoliciesTreeItem]; + } + + public hasMoreChildrenImpl(): boolean { + return false; + } + + public pickTreeItemImpl(expectedContextValues: (string | RegExp)[]): AzureTreeItem | undefined { + for (const expectedContextValue of expectedContextValues) { + switch (expectedContextValue) { + case AuthorizationAccessPolicyTreeItem.contextValue: + return this.authorizationAccessPoliciesTreeItem; + default: + } + } + return undefined; + } + + public async deleteTreeItemImpl(): Promise { + const message: string = localize("confirmAuthorizationRemove", `Are you sure you want to remove Authorization '${this.authorizationContract.name}' from Authorization provider '${this.root.authorizationProviderName}'?`); + const result = await window.showWarningMessage(message, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel); + if (result === DialogResponses.deleteResponse) { + const deletingMessage: string = localize("removingAuthorization", `Removing Authorization "${this.authorizationContract.name}" from Authorization provider '${this.root.authorizationProviderName}.'`); + await window.withProgress({ location: ProgressLocation.Notification, title: deletingMessage }, async () => { + const apimService = new ApimService( + this.root.credentials, + this.root.environment.resourceManagerEndpointUrl, + this.root.subscriptionId, + this.root.resourceGroupName, + this.root.serviceName); + await apimService.deleteAuthorization(this.root.authorizationProviderName, nonNullProp(this.authorizationContract, "name")); + }); + // don't wait + window.showInformationMessage(localize("removedAuthorization", `Successfully removed authorization "${this.authorizationContract.name}" from Authorization provider '${this.root.authorizationProviderName}'.`)); + + } else { + throw new UserCancelledError(); + } + } + + private createRoot(subRoot: ISubscriptionContext): IAuthorizationTreeRoot { + return Object.assign({}, subRoot, { + authorizationName: nonNullProp(this.authorizationContract, 'name') + }); + } +} diff --git a/src/explorer/AuthorizationsTreeItem.ts b/src/explorer/AuthorizationsTreeItem.ts new file mode 100644 index 0000000..4c9c5eb --- /dev/null +++ b/src/explorer/AuthorizationsTreeItem.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProgressLocation, window } from "vscode"; +import { AzExtTreeItem, AzureParentTreeItem, ICreateChildImplContext } from "vscode-azureextensionui"; +import { ApimService } from "../azure/apim/ApimService"; +import { IAuthorizationContract, IAuthorizationPropertiesContract, IAuthorizationProviderContract, IGrantTypesContract, ITokenStoreIdentityProviderContract } from "../azure/apim/contracts"; +import { askAuthorizationParameterValues, askId, IParameterValues } from "../commands/authorizations/common"; +import { localize } from "../localize"; +import { processError } from "../utils/errorUtil"; +import { nonNullValue } from "../utils/nonNull"; +import { treeUtils } from "../utils/treeUtils"; +import { AuthorizationProviderTreeItem } from "./AuthorizationProviderTreeItem"; +import { AuthorizationTreeItem } from "./AuthorizationTreeItem"; +import { IAuthorizationProviderTreeRoot } from "./IAuthorizationProviderTreeRoot"; + +export interface IAuthorizationTreeItemContext extends ICreateChildImplContext { + authorizationName: string; + authorization: IAuthorizationPropertiesContract; +} + +export class AuthorizationsTreeItem extends AzureParentTreeItem { + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('list'); + } + public static contextValue: string = 'azureApiManagementAuthorizations'; + public label: string = "Authorizations"; + public contextValue: string = AuthorizationsTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.Authorization', 'Authorization'); + private _nextLink: string | undefined; + private apimService: ApimService; + + public hasMoreChildrenImpl(): boolean { + return this._nextLink !== undefined; + } + + public async loadMoreChildrenImpl(clearCache: boolean): Promise { + if (clearCache) { + this._nextLink = undefined; + } + + this.apimService = new ApimService( + this.root.credentials, + this.root.environment.resourceManagerEndpointUrl, + this.root.subscriptionId, + this.root.resourceGroupName, + this.root.serviceName); + + const authorizations: IAuthorizationContract[] = await this.apimService.listAuthorizations(this.root.authorizationProviderName); + + return this.createTreeItemsWithErrorHandling( + authorizations, + "invalidApiManagementAuthorization", + async (authorization: IAuthorizationContract) => new AuthorizationTreeItem(this, authorization), + (authorization: IAuthorizationContract) => { + return authorization.name; + }); + } + + public async createChildImpl(context: IAuthorizationTreeItemContext): Promise { + await this.buildContext(context); + if (context.authorizationName !== null + && context.authorization !== null) { + return window.withProgress( + { + location: ProgressLocation.Notification, + title: localize("creatingAuthorization", `Creating Authorization '${context.authorizationName}' under Authorization Provider ${this.root.authorizationProviderName} ...`), + cancellable: false + }, + // tslint:disable-next-line:no-non-null-assertion + async () => { + const authorizationName = context.authorizationName; + context.showCreatingTreeItem(authorizationName); + try { + const apimService = new ApimService(this.root.credentials, this.root.environment.resourceManagerEndpointUrl, this.root.subscriptionId, this.root.resourceGroupName, this.root.serviceName); + let authorization = await apimService.getAuthorization(this.root.authorizationProviderName, authorizationName); + if (authorization === undefined) { + authorization = await apimService.createAuthorization(this.root.authorizationProviderName, authorizationName, context.authorization); + window.showInformationMessage(localize("createdAuthorization", `Created Authorization '${authorizationName}' succesfully.`)); + return new AuthorizationTreeItem(this, authorization); + } else { + throw new Error(localize("createAuthorization", `Authorization '${authorizationName}' already exists.`)); + } + } catch (error) { + throw new Error(processError(error, localize("createAuthorization", `Failed to add authorization '${authorizationName}' to Authorization provider '${this.root.authorizationProviderName}'.`))); + } + } + ); + } else { + throw Error("Expected Authorization name."); + } + } + + private async buildContext(context: IAuthorizationTreeItemContext): Promise { + const authorizationProvider: IAuthorizationProviderContract = (this.parent).authorizationProviderContract; + const authorizationName = await askId('Enter Authorization name ...', 'Invalid Authorization name ...'); + context.authorizationName = authorizationName; + let parameterValues: IParameterValues = {}; + let grantType = IGrantTypesContract.authorizationCode; + if (authorizationProvider.properties.oauth2?.grantTypes.clientCredentials) { + grantType = IGrantTypesContract.clientCredentials; + const identityProvider: ITokenStoreIdentityProviderContract = await this.apimService.getTokenStoreIdentityProvider(authorizationProvider.properties.identityProvider); + const grant = identityProvider.properties.oauth2.grantTypes.clientCredentials; + parameterValues = await askAuthorizationParameterValues(nonNullValue(grant)); + } + context.authorization = { authorizationType: "oauth2", oauth2grantType: grantType, parameters: parameterValues }; + } +} diff --git a/src/explorer/IAuthorizationAccessPolicyTreeRoot.ts b/src/explorer/IAuthorizationAccessPolicyTreeRoot.ts new file mode 100644 index 0000000..3e0a9bd --- /dev/null +++ b/src/explorer/IAuthorizationAccessPolicyTreeRoot.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAuthorizationTreeRoot } from "./IAuthorizationTreeRoot"; + +export interface IAuthorizationAccessPolicyTreeRoot extends IAuthorizationTreeRoot { + accessPolicyName: string; +} diff --git a/src/explorer/IAuthorizationProviderTreeRoot.ts b/src/explorer/IAuthorizationProviderTreeRoot.ts new file mode 100644 index 0000000..d6274ae --- /dev/null +++ b/src/explorer/IAuthorizationProviderTreeRoot.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IServiceTreeRoot } from "./IServiceTreeRoot"; + +export interface IAuthorizationProviderTreeRoot extends IServiceTreeRoot { + authorizationProviderName: string; +} diff --git a/src/explorer/IAuthorizationTreeRoot.ts b/src/explorer/IAuthorizationTreeRoot.ts new file mode 100644 index 0000000..eec64a1 --- /dev/null +++ b/src/explorer/IAuthorizationTreeRoot.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAuthorizationProviderTreeRoot } from "./IAuthorizationProviderTreeRoot"; + +export interface IAuthorizationTreeRoot extends IAuthorizationProviderTreeRoot { + authorizationName: string; +} diff --git a/src/explorer/ServiceTreeItem.ts b/src/explorer/ServiceTreeItem.ts index 2713eeb..98f57a6 100644 --- a/src/explorer/ServiceTreeItem.ts +++ b/src/explorer/ServiceTreeItem.ts @@ -14,6 +14,10 @@ import { ApiOperationTreeItem } from "./ApiOperationTreeItem"; import { ApiPolicyTreeItem } from "./ApiPolicyTreeItem"; import { ApisTreeItem } from "./ApisTreeItem"; import { ApiTreeItem } from "./ApiTreeItem"; +import { AuthorizationAccessPolicyTreeItem } from "./AuthorizationAccessPolicyTreeItem"; +import { AuthorizationProvidersTreeItem } from "./AuthorizationProvidersTreeItem"; +import { AuthorizationProviderTreeItem } from "./AuthorizationProviderTreeItem"; +import { AuthorizationTreeItem } from "./AuthorizationTreeItem"; import { GatewaysTreeItem } from "./GatewaysTreeItem"; import { IServiceTreeRoot } from "./IServiceTreeRoot"; import { NamedValuesTreeItem } from "./NamedValuesTreeItem"; @@ -47,6 +51,7 @@ export class ServiceTreeItem extends AzureParentTreeItem { public readonly productsTreeItem: ProductsTreeItem; public readonly gatewaysTreeItem: GatewaysTreeItem; public readonly subscriptionsTreeItem: SubscriptionsTreeItem; + public readonly authorizationProvidersTreeItem: AuthorizationProvidersTreeItem; private _root: IServiceTreeRoot; @@ -62,6 +67,7 @@ export class ServiceTreeItem extends AzureParentTreeItem { this.productsTreeItem = new ProductsTreeItem(this); this.namedValuesTreeItem = new NamedValuesTreeItem(this); this.subscriptionsTreeItem = new SubscriptionsTreeItem(this); + this.authorizationProvidersTreeItem = new AuthorizationProvidersTreeItem(this); //parent.iconPath = const sku = nonNullValue(this.apiManagementService.sku.name); @@ -76,9 +82,9 @@ export class ServiceTreeItem extends AzureParentTreeItem { public async loadMoreChildrenImpl(): Promise[]> { if (this.gatewaysTreeItem === undefined) { - return [this.apisTreeItem, this.namedValuesTreeItem, this.productsTreeItem, this.servicePolicyTreeItem, this.subscriptionsTreeItem]; + return [this.apisTreeItem, this.namedValuesTreeItem, this.productsTreeItem, this.servicePolicyTreeItem, this.subscriptionsTreeItem, this.authorizationProvidersTreeItem]; } - return [this.apisTreeItem, this.namedValuesTreeItem, this.productsTreeItem, this.servicePolicyTreeItem, this.gatewaysTreeItem, this.subscriptionsTreeItem]; + return [this.apisTreeItem, this.namedValuesTreeItem, this.productsTreeItem, this.servicePolicyTreeItem, this.gatewaysTreeItem, this.subscriptionsTreeItem, this.authorizationProvidersTreeItem]; } public hasMoreChildrenImpl(): boolean { @@ -124,6 +130,10 @@ export class ServiceTreeItem extends AzureParentTreeItem { case ProductTreeItem.contextValue: case ProductPolicyTreeItem.contextValue: return this.productsTreeItem; + case AuthorizationProviderTreeItem.contextValue: + case AuthorizationTreeItem.contextValue: + case AuthorizationAccessPolicyTreeItem.contextValue: + return this.authorizationProvidersTreeItem; default: } } diff --git a/src/explorer/editors/arm/AuthorizationAccessPolicyResourceEditor.ts b/src/explorer/editors/arm/AuthorizationAccessPolicyResourceEditor.ts new file mode 100644 index 0000000..cf60b1a --- /dev/null +++ b/src/explorer/editors/arm/AuthorizationAccessPolicyResourceEditor.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { AzureTreeItem } from "vscode-azureextensionui"; +import { ApimService } from "../../../azure/apim/ApimService"; +import { IAuthorizationAccessPolicyContract } from "../../../azure/apim/contracts"; + +import { nonNullValue } from "../../../utils/nonNull"; +import { IAuthorizationAccessPolicyTreeRoot } from "../../IAuthorizationAccessPolicyTreeRoot"; +import { BaseArmResourceEditor } from "./BaseArmResourceEditor"; + +// tslint:disable-next-line:no-any +export class AuthorizationAccessPolicyResourceEditor extends BaseArmResourceEditor { + public entityType: string = "AuthorizationAccessPolicy"; + constructor() { + super(); + } + + public async getDataInternal(context: AzureTreeItem): Promise { + const apimService = new ApimService( + context.root.credentials, + context.root.environment.resourceManagerEndpointUrl, + context.root.subscriptionId, + context.root.resourceGroupName, + context.root.serviceName); + + const response = await apimService.getAuthorizationAccessPolicy(context.root.authorizationProviderName, context.root.authorizationName, context.root.accessPolicyName); + return nonNullValue(response); + } + + public async updateDataInternal(context: AzureTreeItem, payload: IAuthorizationAccessPolicyContract): Promise { + const apimService = new ApimService( + context.root.credentials, + context.root.environment.resourceManagerEndpointUrl, + context.root.subscriptionId, + context.root.resourceGroupName, + context.root.serviceName); + + const response = await apimService.createAuthorizationAccessPolicy(context.root.authorizationProviderName, context.root.authorizationName, context.root.accessPolicyName, payload.properties); + return nonNullValue(response); + } +} diff --git a/src/explorer/editors/arm/AuthorizationProviderResourceEditor.ts b/src/explorer/editors/arm/AuthorizationProviderResourceEditor.ts new file mode 100644 index 0000000..a4ccf8b --- /dev/null +++ b/src/explorer/editors/arm/AuthorizationProviderResourceEditor.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { AzureTreeItem } from "vscode-azureextensionui"; +import { ApimService } from "../../../azure/apim/ApimService"; +import { IAuthorizationProviderContract } from "../../../azure/apim/contracts"; +import { nonNullValue } from "../../../utils/nonNull"; +import { IAuthorizationProviderTreeRoot } from "../../IAuthorizationProviderTreeRoot"; +import { BaseArmResourceEditor } from "./BaseArmResourceEditor"; + +// tslint:disable-next-line:no-any +export class AuthorizationProviderResourceEditor extends BaseArmResourceEditor { + public entityType: string = "AuthorizationProvider"; + constructor() { + super(); + } + + public async getDataInternal(context: AzureTreeItem): Promise { + const apimService = new ApimService( + context.root.credentials, + context.root.environment.resourceManagerEndpointUrl, + context.root.subscriptionId, + context.root.resourceGroupName, + context.root.serviceName); + + const response = await apimService.getAuthorizationProvider(context.root.authorizationProviderName); + return nonNullValue(response); + } + + public async updateDataInternal(context: AzureTreeItem, payload: IAuthorizationProviderContract): Promise { + const apimService = new ApimService( + context.root.credentials, + context.root.environment.resourceManagerEndpointUrl, + context.root.subscriptionId, + context.root.resourceGroupName, + context.root.serviceName); + + const response = await apimService.createAuthorizationProvider(context.root.authorizationProviderName, payload.properties); + return nonNullValue(response); + } +} diff --git a/src/explorer/editors/arm/AuthorizationResourceEditor.ts b/src/explorer/editors/arm/AuthorizationResourceEditor.ts new file mode 100644 index 0000000..06df512 --- /dev/null +++ b/src/explorer/editors/arm/AuthorizationResourceEditor.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { AzureTreeItem } from "vscode-azureextensionui"; +import { ApimService } from "../../../azure/apim/ApimService"; +import { IAuthorizationContract } from "../../../azure/apim/contracts"; + +import { nonNullValue } from "../../../utils/nonNull"; +import { IAuthorizationTreeRoot } from "../../IAuthorizationTreeRoot"; +import { BaseArmResourceEditor } from "./BaseArmResourceEditor"; + +// tslint:disable-next-line:no-any +export class AuthorizationResourceEditor extends BaseArmResourceEditor { + public entityType: string = "Authorization"; + constructor() { + super(); + } + + public async getDataInternal(context: AzureTreeItem): Promise { + const apimService = new ApimService( + context.root.credentials, + context.root.environment.resourceManagerEndpointUrl, + context.root.subscriptionId, + context.root.resourceGroupName, + context.root.serviceName); + + const response = await apimService.getAuthorization(context.root.authorizationProviderName, context.root.authorizationName); + return nonNullValue(response); + } + + public async updateDataInternal(context: AzureTreeItem, payload: IAuthorizationContract): Promise { + const apimService = new ApimService( + context.root.credentials, + context.root.environment.resourceManagerEndpointUrl, + context.root.subscriptionId, + context.root.resourceGroupName, + context.root.serviceName); + + const response = await apimService.createAuthorization(context.root.authorizationProviderName, context.root.authorizationName, payload.properties); + return nonNullValue(response); + } +} diff --git a/src/extension.ts b/src/extension.ts index 966425c..6b3454c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,11 +6,18 @@ 'use strict'; // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below +import * as query from 'querystring'; import * as vscode from 'vscode'; import { AzExtTreeDataProvider, AzureParentTreeItem, AzureTreeItem, AzureUserInput, callWithTelemetryAndErrorHandling, createAzExtOutputChannel, IActionContext, registerCommand, registerEvent, registerUIExtensionVariables } from 'vscode-azureextensionui'; import { addApiFilter } from './commands/addApiFilter'; import { addApiToGateway } from './commands/addApiToGateway'; import { addApiToProduct } from './commands/addApiToProduct'; +import { authorizeAuthorization } from './commands/authorizations/authorizeAuthorization'; +import { copyAuthorizationPolicy } from './commands/authorizations/copyAuthorizationPolicy'; +import { copyAuthorizationProviderRedirectUrl } from './commands/authorizations/copyAuthorizationProviderRedirectUrl'; +import { createAuthorization } from './commands/authorizations/createAuthorization'; +import { createAuthorizationAccessPolicy } from './commands/authorizations/createAuthorizationAccessPolicy'; +import { createAuthorizationProvider } from './commands/authorizations/createAuthorizationProvider'; import { copySubscriptionKey } from './commands/copySubscriptionKey'; import { createService } from './commands/createService'; import { debugPolicy } from './commands/debugPolicies/debugPolicy'; @@ -38,8 +45,17 @@ import { ApiOperationTreeItem } from './explorer/ApiOperationTreeItem'; import { ApiPolicyTreeItem } from './explorer/ApiPolicyTreeItem'; import { ApisTreeItem } from './explorer/ApisTreeItem'; import { ApiTreeItem } from './explorer/ApiTreeItem'; +import { AuthorizationAccessPoliciesTreeItem } from './explorer/AuthorizationAccessPoliciesTreeItem'; +import { AuthorizationAccessPolicyTreeItem } from './explorer/AuthorizationAccessPolicyTreeItem'; +import { AuthorizationProvidersTreeItem } from './explorer/AuthorizationProvidersTreeItem'; +import { AuthorizationProviderTreeItem } from './explorer/AuthorizationProviderTreeItem'; +import { AuthorizationsTreeItem } from './explorer/AuthorizationsTreeItem'; +import { AuthorizationTreeItem } from './explorer/AuthorizationTreeItem'; import { AzureAccountTreeItem } from './explorer/AzureAccountTreeItem'; import { ApiResourceEditor } from './explorer/editors/arm/ApiResourceEditor'; +import { AuthorizationAccessPolicyResourceEditor } from './explorer/editors/arm/AuthorizationAccessPolicyResourceEditor'; +import { AuthorizationProviderResourceEditor } from './explorer/editors/arm/AuthorizationProviderResourceEditor'; +import { AuthorizationResourceEditor } from './explorer/editors/arm/AuthorizationResourceEditor'; import { OperationResourceEditor } from './explorer/editors/arm/OperationResourceEditor'; import { ProductResourceEditor } from './explorer/editors/arm/ProductResourceEditor'; import { OpenApiEditor } from './explorer/editors/openApi/OpenApiEditor'; @@ -61,6 +77,7 @@ import { ServicePolicyTreeItem } from './explorer/ServicePolicyTreeItem'; import { ServiceTreeItem } from './explorer/ServiceTreeItem'; import { SubscriptionTreeItem } from './explorer/SubscriptionTreeItem'; import { ext } from './extensionVariables'; +import { localize } from './localize'; // this method is called when your extension is activated // your extension is activated the very first time the command is executed @@ -86,8 +103,13 @@ export async function activateInternal(context: vscode.ExtensionContext) { registerCommands(ext.tree); registerEditors(context); - activate(context); // activeta debug context + const handler = new UriEventHandler(); + context.subscriptions.push( + vscode.window.registerUriHandler(handler) + ); + + activate(context); // activeta debug context }); } @@ -133,6 +155,16 @@ function registerCommands(tree: AzExtTreeDataProvider): void { registerCommand('azureApiManagement.setCustomHostName', setCustomHostName); registerCommand('azureApiManagement.createSubscription', createSubscription); registerCommand('azureApiManagement.deleteSubscription', async (context: IActionContext, node?: AzureTreeItem) => await deleteNode(context, SubscriptionTreeItem.contextValue, node)); + + registerCommand('azureApiManagement.createAuthorizationProvider', async (context: IActionContext, node?: AuthorizationProvidersTreeItem) => { await createAuthorizationProvider(context, node); }); + registerCommand('azureApiManagement.createAuthorization', async (context: IActionContext, node?: AuthorizationsTreeItem) => { await createAuthorization(context, node); }); + registerCommand('azureApiManagement.createAuthorizationAccessPolicy', async (context: IActionContext, node?: AuthorizationAccessPoliciesTreeItem) => { await createAuthorizationAccessPolicy(context, node); }); + registerCommand('azureApiManagement.deleteAuthorizationProvider', async (context: IActionContext, node?: AzureTreeItem) => await deleteNode(context, AuthorizationProviderTreeItem.contextValue, node)); + registerCommand('azureApiManagement.copyAuthorizationProviderRedirectUrl', async (context: IActionContext, node?: AuthorizationProviderTreeItem) => await copyAuthorizationProviderRedirectUrl(context, node)); + registerCommand('azureApiManagement.authorizeAuthorization', async (context: IActionContext, node?: AuthorizationTreeItem) => { await authorizeAuthorization(context, node); }); + registerCommand('azureApiManagement.copyAuthorizationPolicy', async (context: IActionContext, node?: AuthorizationTreeItem) => { await copyAuthorizationPolicy(context, node); }); + registerCommand('azureApiManagement.deleteAuthorization', async (context: IActionContext, node?: AzureTreeItem) => await deleteNode(context, AuthorizationTreeItem.contextValue, node)); + registerCommand('azureApiManagement.deleteAuthorizationAccessPolicy', async (context: IActionContext, node?: AzureTreeItem) => await deleteNode(context, AuthorizationAccessPolicyTreeItem.contextValue, node)); } // tslint:disable-next-line: max-func-body-length @@ -248,9 +280,72 @@ function registerEditors(context: vscode.ExtensionContext) : void { await productPolicyEditor.showEditor(node); vscode.commands.executeCommand('setContext', 'isEditorEnabled', true); }, doubleClickDebounceDelay); + + const authorizationProviderResourceEditor: AuthorizationProviderResourceEditor = new AuthorizationProviderResourceEditor(); + context.subscriptions.push(authorizationProviderResourceEditor); + registerEvent('azureApiManagement.AuthorizationProviderResourceEditor.onDidSaveTextDocument', + vscode.workspace.onDidSaveTextDocument, async (actionContext: IActionContext, doc: vscode.TextDocument) => { + await authorizationProviderResourceEditor.onDidSaveTextDocument(actionContext, context.globalState, doc); }); + + registerCommand('azureApiManagement.showArmAuthorizationProvider', async (actionContext: IActionContext, node?: AuthorizationProviderTreeItem) => { + if (!node) { + node = await ext.tree.showTreeItemPicker(AuthorizationProviderTreeItem.contextValue, actionContext); + } + await authorizationProviderResourceEditor.showEditor(node); + vscode.commands.executeCommand('setContext', 'isEditorEnabled', true); + }, doubleClickDebounceDelay); + + const authorizationResourceEditor: AuthorizationResourceEditor = new AuthorizationResourceEditor(); + context.subscriptions.push(authorizationResourceEditor); + registerEvent('azureApiManagement.AuthorizationResourceEditor.onDidSaveTextDocument', + vscode.workspace.onDidSaveTextDocument, async (actionContext: IActionContext, doc: vscode.TextDocument) => { + await authorizationResourceEditor.onDidSaveTextDocument(actionContext, context.globalState, doc); }); + + registerCommand('azureApiManagement.showArmAuthorization', async (actionContext: IActionContext, node?: AuthorizationTreeItem) => { + if (!node) { + node = await ext.tree.showTreeItemPicker(AuthorizationTreeItem.contextValue, actionContext); + } + await authorizationResourceEditor.showEditor(node); + vscode.commands.executeCommand('setContext', 'isEditorEnabled', true); + }, doubleClickDebounceDelay); + + const authorizationAccessPolicyResourceEditor: AuthorizationAccessPolicyResourceEditor = new AuthorizationAccessPolicyResourceEditor(); + context.subscriptions.push(authorizationAccessPolicyResourceEditor); + registerEvent('azureApiManagement.AuthorizationAccessPolicyResourceEditor.onDidSaveTextDocument', + vscode.workspace.onDidSaveTextDocument, async (actionContext: IActionContext, doc: vscode.TextDocument) => { + await authorizationAccessPolicyResourceEditor.onDidSaveTextDocument(actionContext, context.globalState, doc); }); + + registerCommand('azureApiManagement.showArmAuthorizationAccessPolicy', async (actionContext: IActionContext, node?: AuthorizationAccessPolicyTreeItem) => { + if (!node) { + node = await ext.tree.showTreeItemPicker(AuthorizationAccessPolicyTreeItem.contextValue, actionContext); + } + await authorizationAccessPolicyResourceEditor.showEditor(node); + vscode.commands.executeCommand('setContext', 'isEditorEnabled', true); + }, doubleClickDebounceDelay); } // this method is called when your extension is deactivated // tslint:disable:typedef // tslint:disable-next-line:no-empty export function deactivateInternal() {} + +class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { + public handleUri(uri: vscode.Uri) { + if (uri.path.startsWith('/vscodeauthcomplete')) { + if (uri.query !== null && uri.query.includes('error')) { + const queryParams = >query.parse(uri.query); + // tslint:disable-next-line:no-string-literal + const errorValue = queryParams['error']; + const errorDecoded = new Buffer(errorValue, 'base64'); + ext.outputChannel.appendLine(localize('authFailed', `Authorization failed. ${errorDecoded.toString('utf8')}.`)); + vscode.window.showInformationMessage(localize('authFailed', `Authorization failed. ${errorDecoded.toString('utf8')}.`)); + } else { + ext.outputChannel.appendLine(localize('authComplete', `Authorization success. ${uri.path}`)); + const authProvider = uri.path.split('/')[2]; + const authorization = uri.path.split('/')[3]; + vscode.window.showInformationMessage(localize('authSuccess', `Authorized '${authorization}' under Authorization Provider '${authProvider}'.`)); + vscode.window.showInformationMessage(localize('closeBrowserWindow', `You can now close the browser window that was launched during the authorization process.`)); + } + } + } +}