Skip to content

Commit

Permalink
Validate web site preview URL (#1003)
Browse files Browse the repository at this point in the history
* Validate website preview URL

* formatting updates
  • Loading branch information
tyaginidhi committed Jul 26, 2024
1 parent 6730f59 commit 1dcf5da
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 72 deletions.
1 change: 1 addition & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Preview site": "Preview site",
"Open in Power Pages studio": "Open in Power Pages studio",
"Preview site URL is not available": "Preview site URL is not available",
"Preview site URL is not valid": "Preview site URL is not valid",
"Opening preview site...": "Opening preview site...",
"Power Pages studio URL is not available": "Power Pages studio URL is not available",
"Microsoft wants your feeback": "Microsoft wants your feeback",
Expand Down
3 changes: 3 additions & 0 deletions loc/translations-export/vscode-powerplatform.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ The {3} represents Dataverse Environment's Organization ID (GUID)</note>
<trans-unit id="++CODE++f190e8061b8cbadd991ff217210525eff0000a8c6ddc2d44baec8388b7bd1a3e">
<source xml:lang="en">Preview site URL is not available</source>
</trans-unit>
<trans-unit id="++CODE++1cc20af81855b8570858ea1ac0491000d230bcf2ac53c1c1b30243a9b58a2036">
<source xml:lang="en">Preview site URL is not valid</source>
</trans-unit>
<trans-unit id="++CODE++7a6098eb5ff2c2401890216bb502ce6583ff7bddc99e62f8751551eab45ae1b4">
<source xml:lang="en">Profile Kind: {0}</source>
<note>The {0} represents the profile type (Admin vs Dataverse)</note>
Expand Down
7 changes: 3 additions & 4 deletions src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import { oneDSLoggerWrapper } from "../common/OneDSLoggerTelemetry/oneDSLoggerWr
import { OrgChangeNotifier, orgChangeEvent } from "./OrgChangeNotifier";
import { ActiveOrgOutput } from "./pac/PacTypes";
import { desktopTelemetryEventNames } from "../common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames";
import { IArtemisAPIOrgResponse } from "../common/services/Interfaces";
import { ArtemisService } from "../common/services/ArtemisService";
import { workspaceContainsPortalConfigFolder } from "../common/utilities/PathFinderUtil";
import { getPortalsOrgURLs } from "../common/utilities/WorkspaceInfoFinderUtil";
Expand Down Expand Up @@ -190,9 +189,9 @@ export async function activate(
_context.subscriptions.push(
orgChangeEvent(async (orgDetails: ActiveOrgOutput) => {
const orgID = orgDetails.OrgId;
const artemisResponse = await ArtemisService.fetchArtemisResponse(orgID, _telemetry);
if (artemisResponse !== null && artemisResponse.length > 0) {
const { geoName, geoLongName } = artemisResponse[0]?.response as unknown as IArtemisAPIOrgResponse;
const artemisResponse = await ArtemisService.getArtemisResponse(orgID, _telemetry, "");
if (artemisResponse !== null && artemisResponse.response !== null) {
const { geoName, geoLongName } = artemisResponse.response;
oneDSLoggerWrapper.instantiate(geoName, geoLongName);
oneDSLoggerWrapper.getLogger().traceInfo(desktopTelemetryEventNames.DESKTOP_EXTENSION_INIT_CONTEXT, { ...orgDetails, orgGeo: geoName });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export enum webExtensionTelemetryEventNames {
WEB_EXTENSION_MULTI_FILE_FEATURE_FLAG_ENABLED = "WebExtensionMultiFileFeatureFlagEnabled",
WEB_EXTENSION_MULTI_FILE_FEATURE_FLAG_DISABLED = "WebExtensionMultiFileFeatureFlagDisabled",
WEB_EXTENSION_MULTI_FILE_MANDATORY_PARAMETERS_MISSING = "WebExtensionMultiFileMandatoryParametersMissing",
WEB_EXTENSION_WEBSITE_PREVIEW_URL_VALIDATION_SITE_DETAILS_FETCH_FAILED = "WebExtensionWebsitePreviewUrlValidationSiteDetailsFetchFailed",
WEB_EXTENSION_WEBSITE_PREVIEW_URL_VALIDATION_INSUFFICIENT_PARAMETERS = "WebExtensionWebsitePreviewUrlValidationInsufficientParameters",
WEB_EXTENSION_MULTI_FILE_INVALID_DATAVERSE_URL = "WebExtensionMultiFileInvalidDataverseUrl",
WEB_EXTENSION_MULTI_FILE_INVALID_WEBSITE_PREVIEW_URL = "WebExtensionMultiFileInvalidWebsitePreviewUrl",
WEB_EXTENSION_CO_PRESENCE_FEATURE_FLAG_DISABLED = "WebExtensionCoPresenceFeatureFlagDisabled",
Expand All @@ -100,6 +102,8 @@ export enum webExtensionTelemetryEventNames {
WEB_EXTENSION_POWER_PAGES_WEB_VIEW_REGISTER_FAILED = 'webExtensionPowerPagesWebViewRegisterFailed',
WEB_EXTENSION_BACK_TO_STUDIO_TRIGGERED = 'webExtensionBackToStudioTriggered',
WEB_EXTENSION_PREVIEW_SITE_TRIGGERED = 'webExtensionPreviewSiteTriggered',
WEB_EXTENSION_WEBSITE_PREVIEW_URL_INVALID = "WebExtensionWebsitePreviewUrlInvalid",
WEB_EXTENSION_WEBSITE_PREVIEW_URL_UNAVAILABLE = "WebExtensionWebsitePreviewUrlUnavailable",
WEB_EXTENSION_IMAGE_EDIT_SUPPORTED_FILE_EXTENSION = 'webExtensionImageEditSupportedFileExtension',
WEB_EXTENSION_SAVE_IMAGE_FILE_TRIGGERED = 'webExtensionSaveImageFileTriggered',
WEB_EXTENSION_FETCH_GET_OR_CREATE_SHARED_WORK_SPACE_ERROR = 'webExtensionFetchGetOrCreateSharedWorkSpaceError',
Expand Down
72 changes: 34 additions & 38 deletions src/common/services/ArtemisService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,55 +8,52 @@ import { COPILOT_UNAVAILABLE } from "../copilot/constants";
import { ITelemetry } from "../OneDSLoggerTelemetry/telemetry/ITelemetry";
import { sendTelemetryEvent } from "../copilot/telemetry/copilotTelemetry";
import { CopilotArtemisFailureEvent, CopilotArtemisSuccessEvent } from "../copilot/telemetry/telemetryConstants";
import { BAPServiceStamp as BAPAPIEndpointStamp } from "./Constants";
import { IArtemisAPIOrgResponse, IArtemisServiceEndpointInformation, IIntelligenceAPIEndpointInformation } from "./Interfaces";
import { ServiceEndpointCategory } from "./Constants";
import { IArtemisAPIOrgResponse, IArtemisServiceEndpointInformation, IArtemisServiceResponse, IIntelligenceAPIEndpointInformation } from "./Interfaces";
import { isCopilotDisabledInGeo, isCopilotSupportedInGeo } from "../copilot/utils/copilotUtil";
import { BAPService } from "./BAPService";

export class ArtemisService {
public static async getIntelligenceEndpoint(orgId: string, telemetry: ITelemetry, sessionID: string, environmentId: string): Promise<IIntelligenceAPIEndpointInformation> {

const artemisResponses = await ArtemisService.fetchArtemisResponse(orgId, telemetry, sessionID);
const artemisResponse = await ArtemisService.getArtemisResponse(orgId, telemetry, sessionID);

if (artemisResponses === null || artemisResponses.length === 0) {
if (artemisResponse === null) {
return { intelligenceEndpoint: null, geoName: null, crossGeoDataMovementEnabledPPACFlag: false };
}
const { geoName, environment, clusterNumber } = artemisResponse.response as unknown as IArtemisAPIOrgResponse;
sendTelemetryEvent(telemetry, { eventName: CopilotArtemisSuccessEvent, copilotSessionId: sessionID, geoName: String(geoName), orgId: orgId });

const artemisResponse = artemisResponses[0];
if (artemisResponse !== null) {
const { geoName, environment, clusterNumber } = artemisResponse.response as unknown as IArtemisAPIOrgResponse;
sendTelemetryEvent(telemetry, { eventName: CopilotArtemisSuccessEvent, copilotSessionId: sessionID, geoName: String(geoName), orgId: orgId });

const crossGeoDataMovementEnabledPPACFlag = await BAPService.getCrossGeoCopilotDataMovementEnabledFlag(artemisResponse.stamp, telemetry, environmentId);

if (isCopilotDisabledInGeo().includes(geoName)) {
return { intelligenceEndpoint: COPILOT_UNAVAILABLE, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag };
}
else if (crossGeoDataMovementEnabledPPACFlag === true) {
// Do nothing - we can make this call cross geo
}
else if (!isCopilotSupportedInGeo().includes(geoName)) {
return { intelligenceEndpoint: COPILOT_UNAVAILABLE, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag };
}
const crossGeoDataMovementEnabledPPACFlag = await BAPService.getCrossGeoCopilotDataMovementEnabledFlag(artemisResponse.stamp, telemetry, environmentId);

const intelligenceEndpoint = `https://aibuildertextapiservice.${geoName}-${'il' + clusterNumber}.gateway.${environment}.island.powerapps.com/v1.0/${orgId}/appintelligence/chat`

return { intelligenceEndpoint: intelligenceEndpoint, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag };
if (isCopilotDisabledInGeo().includes(geoName)) {
return { intelligenceEndpoint: COPILOT_UNAVAILABLE, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag };
}
else if (crossGeoDataMovementEnabledPPACFlag === true) {
// Do nothing - we can make this call cross geo
}
else if (!isCopilotSupportedInGeo().includes(geoName)) {
return { intelligenceEndpoint: COPILOT_UNAVAILABLE, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag };
}

return { intelligenceEndpoint: null, geoName: null, crossGeoDataMovementEnabledPPACFlag: false };
const intelligenceEndpoint = `https://aibuildertextapiservice.${geoName}-${'il' + clusterNumber}.gateway.${environment}.island.powerapps.com/v1.0/${orgId}/appintelligence/chat`

return { intelligenceEndpoint: intelligenceEndpoint, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag };
}

// Function to fetch Artemis response
public static async fetchArtemisResponse(orgId: string, telemetry: ITelemetry, sessionID = '') {
public static async getArtemisResponse(orgId: string, telemetry: ITelemetry, sessionID: string): Promise<IArtemisServiceResponse | null> {
const endpointDetails = ArtemisService.convertGuidToUrls(orgId);
const artemisResponses = await ArtemisService.fetchIslandInfo(endpointDetails, telemetry, sessionID);

const artemisResponse = await ArtemisService.fetchIslandInfo(endpointDetails, telemetry, sessionID);
if (artemisResponses === null || artemisResponses.length === 0) {
return null;
}

return artemisResponse;
return artemisResponses[0];
}

static async fetchIslandInfo(endpointDetails: IArtemisServiceEndpointInformation[], telemetry: ITelemetry, sessionID: string) {
static async fetchIslandInfo(endpointDetails: IArtemisServiceEndpointInformation[], telemetry: ITelemetry, sessionID: string): Promise<IArtemisServiceResponse[] | null> {

const requestInit: RequestInit = {
method: 'GET',
Expand All @@ -70,22 +67,21 @@ export class ArtemisService {
if (!response.ok) {
throw new Error('Request failed');
}
return { stamp: endpointDetail.stamp, response: await response.json() };
return { stamp: endpointDetail.stamp, response: await response.json() as IArtemisAPIOrgResponse };
} catch (error) {
return null;
}
});

const results = await Promise.all(promises);
const successfulResponses = results.filter(result => result !== null && result.response !== null);
return successfulResponses;
return successfulResponses as IArtemisServiceResponse[];
} catch (error) {
sendTelemetryEvent(telemetry, { eventName: CopilotArtemisFailureEvent, copilotSessionId: sessionID, error: error as Error })
return null;
}
}


/**
* @param orgId
* @returns urls
Expand All @@ -109,13 +105,13 @@ export class ArtemisService {
const dodUrl = `https://${domain}.${nonProdSegment}.organization.api.appsplatform.us/gateway/cluster?app-version=1`;

return [
{ stamp: BAPAPIEndpointStamp.TEST, endpoint: tstUrl },
{ stamp: BAPAPIEndpointStamp.PREPROD, endpoint: preprodUrl },
{ stamp: BAPAPIEndpointStamp.PROD, endpoint: prodUrl },
{ stamp: BAPAPIEndpointStamp.GCC, endpoint: gccUrl },
{ stamp: BAPAPIEndpointStamp.HIGH, endpoint: highUrl },
{ stamp: BAPAPIEndpointStamp.MOONCAKE, endpoint: mooncakeUrl },
{ stamp: BAPAPIEndpointStamp.DOD, endpoint: dodUrl },
{ stamp: ServiceEndpointCategory.TEST, endpoint: tstUrl },
{ stamp: ServiceEndpointCategory.PREPROD, endpoint: preprodUrl },
{ stamp: ServiceEndpointCategory.PROD, endpoint: prodUrl },
{ stamp: ServiceEndpointCategory.GCC, endpoint: gccUrl },
{ stamp: ServiceEndpointCategory.HIGH, endpoint: highUrl },
{ stamp: ServiceEndpointCategory.MOONCAKE, endpoint: mooncakeUrl },
{ stamp: ServiceEndpointCategory.DOD, endpoint: dodUrl },
];
}

Expand Down
56 changes: 53 additions & 3 deletions src/common/services/AuthenticationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ import {
VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_COMPLETED,
VSCODE_EXTENSION_BAP_SERVICE_AUTHENTICATION_COMPLETED,
VSCODE_EXTENSION_BAP_SERVICE_AUTHENTICATION_FAILED,
VSCODE_EXTENSION_DECODE_JWT_TOKEN_FAILED
VSCODE_EXTENSION_DECODE_JWT_TOKEN_FAILED,
VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_COMPLETED,
VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_FAILED
} from "./TelemetryConstants";
import { ERROR_CONSTANTS } from "../ErrorConstants";
import { BAP_SERVICE_SCOPE_DEFAULT, INTELLIGENCE_SCOPE_DEFAULT, PROVIDER_ID, SCOPE_OPTION_CONTACTS_READ, SCOPE_OPTION_DEFAULT, SCOPE_OPTION_OFFLINE_ACCESS, SCOPE_OPTION_USERS_READ_BASIC_ALL } from "./Constants";
import { BAP_SERVICE_SCOPE_DEFAULT, INTELLIGENCE_SCOPE_DEFAULT, PPAPI_WEBSITES_SERVICE_SCOPE_DEFAULT, PROVIDER_ID, SCOPE_OPTION_CONTACTS_READ, SCOPE_OPTION_DEFAULT, SCOPE_OPTION_OFFLINE_ACCESS, SCOPE_OPTION_USERS_READ_BASIC_ALL } from "./Constants";
import jwt_decode from 'jwt-decode';
import { showErrorDialog } from "../utilities/errorHandlerUtil";


export function getCommonHeadersForDataverse(
accessToken: string,
useOctetStreamContentType?: boolean
Expand Down Expand Up @@ -292,3 +293,52 @@ export function getOIDFromToken(token: string, telemetry: ITelemetry) {
}
return "";
}

export async function powerPlatformAPIAuthentication(
telemetry: ITelemetry,
firstTimeAuth = false
): Promise<string> {
let accessToken = "";
try {
let session = await vscode.authentication.getSession(
PROVIDER_ID,
[PPAPI_WEBSITES_SERVICE_SCOPE_DEFAULT],
{ silent: true }
);

if (!session) {
session = await vscode.authentication.getSession(
PROVIDER_ID,
[PPAPI_WEBSITES_SERVICE_SCOPE_DEFAULT],
{ createIfNone: true }
);
}

accessToken = session?.accessToken ?? "";
if (!accessToken) {
throw new Error(ERROR_CONSTANTS.NO_ACCESS_TOKEN);
}

if (firstTimeAuth) {
sendTelemetryEvent(telemetry, {
eventName: VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_COMPLETED,
userId:
session?.account.id.split("/").pop() ??
session?.account.id ??
"",
});
}
} catch (error) {
showErrorDialog(
vscode.l10n.t(
"Authorization Failed. Please run again to authorize it"
),
vscode.l10n.t("There was a permissions problem with the server")
);
sendTelemetryEvent(telemetry,
{ eventName: VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_FAILED, errorMsg: (error as Error).message }
)
}

return accessToken;
}
20 changes: 10 additions & 10 deletions src/common/services/BAPService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import { ITelemetry } from "../OneDSLoggerTelemetry/telemetry/ITelemetry";
import { bapServiceAuthentication, getCommonHeaders } from "./AuthenticationProvider";
import { VSCODE_EXTENSION_GET_BAP_ENDPOINT_UNSUPPORTED_REGION, VSCODE_EXTENSION_GET_CROSS_GEO_DATA_MOVEMENT_ENABLED_FLAG_COMPLETED, VSCODE_EXTENSION_GET_CROSS_GEO_DATA_MOVEMENT_ENABLED_FLAG_FAILED } from "./TelemetryConstants";
import { BAPServiceStamp, BAP_API_VERSION, BAP_SERVICE_COPILOT_CROSS_GEO_FLAG_RELATIVE_URL, BAP_SERVICE_ENDPOINT } from "./Constants";
import { ServiceEndpointCategory, BAP_API_VERSION, BAP_SERVICE_COPILOT_CROSS_GEO_FLAG_RELATIVE_URL, BAP_SERVICE_ENDPOINT } from "./Constants";
import { sendTelemetryEvent } from "../copilot/telemetry/copilotTelemetry";

export class BAPService {
public static async getCrossGeoCopilotDataMovementEnabledFlag(serviceEndpointStamp: BAPServiceStamp, telemetry: ITelemetry, environmentId: string): Promise<boolean> {
public static async getCrossGeoCopilotDataMovementEnabledFlag(serviceEndpointStamp: ServiceEndpointCategory, telemetry: ITelemetry, environmentId: string): Promise<boolean> {

try {
const accessToken = await bapServiceAuthentication(telemetry, true);
Expand All @@ -33,25 +33,25 @@ export class BAPService {
return false;
}

static async getBAPEndpoint(serviceEndpointStamp: BAPServiceStamp, telemetry: ITelemetry, environmentId: string): Promise<string> {
static async getBAPEndpoint(serviceEndpointStamp: ServiceEndpointCategory, telemetry: ITelemetry, environmentId: string): Promise<string> {

let bapEndpoint = "";

switch (serviceEndpointStamp) {
case BAPServiceStamp.TEST:
case ServiceEndpointCategory.TEST:
bapEndpoint = "https://test.api.bap.microsoft.com";
break;
case BAPServiceStamp.PREPROD:
case ServiceEndpointCategory.PREPROD:
bapEndpoint = "https://preprod.api.bap.microsoft.com";
break;
case BAPServiceStamp.PROD:
case ServiceEndpointCategory.PROD:
bapEndpoint = "https://api.bap.microsoft.com";
break;
// All below endpoints are not supported yet
case BAPServiceStamp.DOD:
case BAPServiceStamp.GCC:
case BAPServiceStamp.HIGH:
case BAPServiceStamp.MOONCAKE:
case ServiceEndpointCategory.DOD:
case ServiceEndpointCategory.GCC:
case ServiceEndpointCategory.HIGH:
case ServiceEndpointCategory.MOONCAKE:
default:
sendTelemetryEvent(telemetry, { eventName: VSCODE_EXTENSION_GET_BAP_ENDPOINT_UNSUPPORTED_REGION, data: serviceEndpointStamp });
break;
Expand Down
16 changes: 14 additions & 2 deletions src/common/services/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ export const SCOPE_OPTION_CONTACTS_READ = "Contacts.Read";
export const SCOPE_OPTION_USERS_READ_BASIC_ALL = "User.ReadBasic.All";
export const SCOPE_OPTION_DEFAULT = "/.default";

// BAP API constants
export const BAP_API_VERSION = '2021-04-01';
export const BAP_SERVICE_SCOPE_DEFAULT = "https://api.bap.microsoft.com/.default";//"https://management.core.windows.net/.default";
export const BAP_SERVICE_SCOPE_DEFAULT = "https://api.bap.microsoft.com/.default";
export const BAP_SERVICE_ENDPOINT = `{rootURL}/providers/Microsoft.BusinessAppPlatform/`;
export const BAP_SERVICE_COPILOT_CROSS_GEO_FLAG_RELATIVE_URL = `scopes/admin/environments/{environmentID}?$expand=properties/copilotPolicies&api-version={apiVersion}`;

export enum BAPServiceStamp {
// PPAPI constants
export const PPAPI_WEBSITES_API_VERSION = '2022-03-01-preview';
export const PPAPI_WEBSITES_SERVICE_SCOPE_DEFAULT = "https://api.powerplatform.com/.default";
export const PPAPI_WEBSITES_ENDPOINT = `{rootURL}/powerpages/environments/{environmentId}/websites`;

export enum ServiceEndpointCategory {
NONE = "",
TEST = "test",
PREPROD = "preprod",
PROD = "prod",
Expand All @@ -26,3 +33,8 @@ export enum BAPServiceStamp {
MOONCAKE = "mooncake",
DOD = "dod",
}

export enum WebsiteApplicationType {
Production = "Production",
Trial = "Trial",
}
Loading

0 comments on commit 1dcf5da

Please sign in to comment.