From 07b5c5b83a7ab5d876123742129327feb4f08a07 Mon Sep 17 00:00:00 2001 From: Gustavo Hidalgo Date: Wed, 7 Feb 2024 15:43:44 -0500 Subject: [PATCH 1/3] 2024.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 02b700dd..cac5ee9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pc-datacatalog", - "version": "2024.1.0", + "version": "2024.1.1", "private": true, "proxy": "http://api:7071/", "dependencies": { From b97ceadc19997b524fec2de9efaf11c7afc3afdf Mon Sep 17 00:00:00 2001 From: Matt McFarland Date: Wed, 21 Feb 2024 10:47:10 -0500 Subject: [PATCH 2/3] 2024.1.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 02b700dd..1edaf46a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pc-datacatalog", - "version": "2024.1.0", + "version": "2024.1.2", "private": true, "proxy": "http://api:7071/", "dependencies": { From f68208876f4716b91dae719764a1b4f683f175b7 Mon Sep 17 00:00:00 2001 From: Matt McFarland Date: Wed, 6 Mar 2024 09:27:25 -0500 Subject: [PATCH 3/3] Use api endpoint to generate az maps token --- .env.sample | 4 +-- README.md | 21 ++++++++++-- api/Dockerfile | 15 ++++++-- api/README.md | 12 +++---- api/map-token/__init__.py | 38 +++++++++++++++++++++ api/map-token/function.json | 19 +++++++++++ api/requirements.txt | 1 + docker-compose.yml | 1 + package-lock.json | 2 +- src/pages/Explore/components/Map/helpers.ts | 38 +++++++++++++++++++++ src/pages/Explore/components/Map/index.tsx | 8 +++-- src/utils/constants.js | 6 ++++ 12 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 api/map-token/__init__.py create mode 100644 api/map-token/function.json diff --git a/.env.sample b/.env.sample index c8456f4b..4517057b 100644 --- a/.env.sample +++ b/.env.sample @@ -13,8 +13,8 @@ REACT_APP_API_ROOT=https://planetarycomputer-staging.microsoft.com # Root URL for image function app endpoints REACT_APP_IMAGE_API_ROOT= -# Subscription key for Azure Maps -REACT_APP_AZMAPS_KEY= +# Client Id for Azure Maps +REACT_APP_AZMAPS_CLIENT_ID=0ba71e87-2836-4f72-82b8-8093147375c7 # URL for JHub cloned repo launch (including 'git-pull') REACT_APP_HUB_URL= diff --git a/README.md b/README.md index c332178c..841f8a65 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,29 @@ First, copy `.env.sample` file to `.env`, and ensure the configuration values ar |`REACT_APP_API_ROOT`| | The root URL for the STAC API, either prod, staging or a local instance. If the URL ends in 'stac', this is a special case that is handled by replacing 'stac' with the target service, e.g. 'data' or 'sas' |`REACT_APP_TILER_ROOT`| Optional | The root URL for the data tiler API, if not hosted from the domain of the STAC API. |`REACT_APP_IMAGE_API_ROOT`| PC APIs pcfunc endpoint | The root URL for the image data API for animations. -|`REACT_APP_AZMAPS_KEY`| Retrieve from Azure Portal | The key used to authenticate the Azure Maps inset map on a dataset detail page. +|`REACT_APP_AZMAPS_CLIENT_ID`| Retrieve from Azure Portal | The Client ID used to authenticate against Azure Maps. |`REACT_APP_HUB_URL`| Optional. URL to root Hub instance | Used to enable a request to launch the Hub with a specific git hosted file. |`REACT_APP_ONEDS_TENANT_KEY`| Lookup at | Telemetry key (not needed for dev) |`REACT_APP_AUTH_URL`| Optional. URL to root pc-session-api instance | Used to enable login work. -Run `./scripts/server` to launch a development server. +Run `./scripts/server --api` to launch a development server with a local Azure Functions host running. + +#### Azure Maps + +In the local development setups, the Azure Maps token is generated using the local developer identity. Be sure to +`az login` and `az account set --subscription "Planetary Computer"` to ensure the correct token is generated. Your identity +will also need the "Azure Maps Search and Render Data Reader" permission, which can be set with: + +```sh +USER_NAME=$(az account show --query user.name -o tsv) +az role assignment create \ + --assignee "$USER_NAME" \ + --role "Azure Maps Search and Render Data Reader" \ + --scope "/subscriptions/9da7523a-cb61-4c3e-b1d4-afa5fc6d2da9/resourceGroups/pc-datacatalog-rg/providers/Microsoft.Maps/accounts/pc-datacatalog-azmaps" \ + --subscription "Planetary Computer" +``` + +Note, you may need to assign this role via an identity that has JIT admin privileges enabled against the Planetary Computer subscription. #### Developing against local STAC assets diff --git a/api/Dockerfile b/api/Dockerfile index 4db6b000..6dad46df 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,10 +1,19 @@ -FROM mcr.microsoft.com/azure-functions/python:4-python3.9 +FROM mcr.microsoft.com/azure-cli:cbl-mariner2.0 ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ AzureFunctionsJobHost__Logging__Console__IsEnabled=true +RUN tdnf install libicu unzip wget -y +RUN wget https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.5530/Azure.Functions.Cli.linux-x64.4.0.5530.zip +RUN mkdir -p /usr/local/lib/Azure.Functions.Cli +RUN unzip Azure.Functions.Cli.linux-x64.4.0.5530.zip -d /usr/local/lib/Azure.Functions.Cli +RUN chmod +x /usr/local/lib/Azure.Functions.Cli/func + +ENV PATH="/usr/local/lib/Azure.Functions.Cli:${PATH}" + +RUN python3 -m ensurepip --upgrade COPY requirements.txt / -RUN pip install -r /requirements.txt +RUN pip3 install -r /requirements.txt COPY requirements-dev.txt / -RUN pip install -r /requirements-dev.txt +RUN pip3 install -r /requirements-dev.txt diff --git a/api/README.md b/api/README.md index 9e670df5..71a81c33 100644 --- a/api/README.md +++ b/api/README.md @@ -12,12 +12,12 @@ To set appropriate configuration values for the Function app, copy the `local.se The `local.settings.json` file has the following keys in the Values section: -|Key|KeyVault Key|Purpose| -|---|---|---| -|`NotificationHook`| | URL to send Teams notification on new Account Request -|`AuthAdminUrl`| | URL to the PC ID admin page which contains the signup table. Used in the Teams notification message. -|`SignupUrl`| | URL to POST new user content to on submission -|`SignupToken` | `pc-id--request-auth-token` | Bearer token required to make the above POST request +| Key | KeyVault Key | Purpose | +|--------------------|-----------------------------|------------------------------------------------------------------------------------------------------| +| `NotificationHook` | | URL to send Teams notification on new Account Request | +| `AuthAdminUrl` | | URL to the PC ID admin page which contains the signup table. Used in the Teams notification message. | +| `SignupUrl` | | URL to POST new user content to on submission | +| `SignupToken` | `pc-id--request-auth-token` | Bearer token required to make the above POST request | ### Production diff --git a/api/map-token/__init__.py b/api/map-token/__init__.py new file mode 100644 index 00000000..12021dc9 --- /dev/null +++ b/api/map-token/__init__.py @@ -0,0 +1,38 @@ +import json +import logging +from typing import TypedDict + +import azure.functions as func + +from azure.identity import DefaultAzureCredential +from azure.core.exceptions import ClientAuthenticationError + +logger = logging.getLogger("api.maps-token") +# For performance, exclude checking options we know won't be used +credential = DefaultAzureCredential( + exclude_environment_credential=True, + exclude_developer_cli_credential=True, + exclude_powershell_credential=True, + exclude_visual_studio_code_credential=True, +) + + +class TokenResponse(TypedDict): + token: str + expires_on: int + + +def main(req: func.HttpRequest) -> func.HttpResponse: + + logger.debug("Python HTTP trigger function processed a request.") + try: + logger.debug("Getting azure maps token") + token = credential.get_token("https://atlas.microsoft.com/.default") + logger.debug("Token acquired") + + resp: TokenResponse = {"token": token.token, "expires_on": token.expires_on} + + return func.HttpResponse(status_code=200, body=json.dumps(resp)) + except ClientAuthenticationError: + logger.exception(f"Error getting azure maps token") + return func.HttpResponse("Error getting token", status_code=500) diff --git a/api/map-token/function.json b/api/map-token/function.json new file mode 100644 index 00000000..e75ca8c3 --- /dev/null +++ b/api/map-token/function.json @@ -0,0 +1,19 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/api/requirements.txt b/api/requirements.txt index 287a5e36..9a69776b 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -3,4 +3,5 @@ # Manually managing azure-functions-worker may cause unexpected issues azure-functions==1.11.2 +azure-identity==1.15.0 requests==2.31.0 diff --git a/docker-compose.yml b/docker-compose.yml index 6aa68f1f..a4084828 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,7 @@ services: - "8000:8000" volumes: - ./api:/usr/src + - ~/.azure:/root/.azure networks: pcdc-network: command: func host start --script-root ./ --cors "*" --port 7071 diff --git a/package-lock.json b/package-lock.json index fa773b19..ae4ddf6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "pc-datacatalog", - "version": "2024.1.0", + "version": "2024.1.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/pages/Explore/components/Map/helpers.ts b/src/pages/Explore/components/Map/helpers.ts index 86a01b42..be890ba9 100644 --- a/src/pages/Explore/components/Map/helpers.ts +++ b/src/pages/Explore/components/Map/helpers.ts @@ -1,4 +1,5 @@ import * as atlas from "azure-maps-control"; +import axios from "axios"; import { DATA_URL, REQUEST_ENTITY, X_REQUEST_ENTITY } from "utils/constants"; import { IStacItem } from "types/stac"; import { ILayerState } from "pages/Explore/types"; @@ -33,6 +34,43 @@ export const addEntityHeader = ( return { headers: {}, url: url }; }; +let cachedToken: string | null = null; +let tokenExpiry: number | null = null; + +export const fetchMapToken = async ( + resolve: (value: string) => void, + reject: (reason?: any) => void +): Promise => { + const nowInSeconds = Math.floor(Date.now() / 1000); // Current time in seconds since epoch + + // Check if we have a valid token in the cache + if (cachedToken !== null && tokenExpiry !== null && nowInSeconds < tokenExpiry) { + resolve(cachedToken); + return; + } + + // If no valid cached token, fetch a new one + try { + const resp = await axios.get<{ token: string; expires_on: number }>( + "./api/map-token" + ); + + if (resp.status === 200 && resp.data.token && resp.data.expires_on) { + cachedToken = resp.data.token; + + // Subtract a small buffer (e.g., 5 minutes in seconds) to ensure + // we refresh the token before it actually expires + tokenExpiry = resp.data.expires_on - 5 * 60; + + resolve(cachedToken); + } else { + reject(new Error("Failed to fetch map token")); + } + } catch (error) { + reject(error); + } +}; + export const makeLayerId = (id: string) => `${mosaicLayerPrefix}${id}`; export const makeDatasourceId = (mapLayerId: string) => `${mapLayerId}-ds`; export const makeLayerOutlineId = (mapLayerId: string) => `${mapLayerId}-outline`; diff --git a/src/pages/Explore/components/Map/index.tsx b/src/pages/Explore/components/Map/index.tsx index 921cb686..683b76ee 100644 --- a/src/pages/Explore/components/Map/index.tsx +++ b/src/pages/Explore/components/Map/index.tsx @@ -28,8 +28,9 @@ import MapSettingsControl from "./components/MapSettingsControl"; import { DEFAULT_MAP_STYLE } from "pages/Explore/utils/constants"; import LegendControl from "./components/LegendControl"; import { MobileViewSidebarButton } from "../MobileViewInMap/ViewInMap.index"; -import { addEntityHeader } from "./helpers"; +import { addEntityHeader, fetchMapToken } from "./helpers"; import { PreviewMessage } from "./components/ItemPreview/PreviewMessage"; +import { AZMAPS_CLIENT_ID } from "utils/constants"; const mapContainerId: string = "viewer-map"; @@ -57,8 +58,9 @@ const ExploreMap = () => { style: DEFAULT_MAP_STYLE, renderWorldCopies: true, authOptions: { - authType: atlas.AuthenticationType.subscriptionKey, - subscriptionKey: process.env.REACT_APP_AZMAPS_KEY, + authType: atlas.AuthenticationType.anonymous, + clientId: AZMAPS_CLIENT_ID, + getToken: fetchMapToken, }, transformRequest: addEntityHeader, }); diff --git a/src/utils/constants.js b/src/utils/constants.js index e863a1bb..37be4cb3 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -21,6 +21,12 @@ export const IMAGE_URL = process.env.REACT_APP_IMAGE_API_ROOT || ""; export const HUB_URL = process.env.REACT_APP_HUB_URL || ""; export const AUTH_URL = process.env.REACT_APP_AUTH_URL || apiRoot; +export const AZMAPS_CLIENT_ID = process.env.REACT_APP_AZMAPS_CLIENT_ID; + +if (!AZMAPS_CLIENT_ID) { + console.warn("AZMAPS_CLIENT_ID must be set"); +} + export const X_REQUEST_ENTITY = "X-PC-Request-Entity"; export const QS_REQUEST_ENTITY = "request_entity"; export const REQUEST_ENTITY = "explorer";