Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate web site preview URL #1003

Merged
merged 4 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
57 changes: 55 additions & 2 deletions src/common/services/AuthenticationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ 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";

Expand Down Expand Up @@ -292,3 +294,54 @@ 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 PPAPI_WEBSITES_SERVICE_SCOPE_DEFAULT = "https://api.powerplatform.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_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
Loading