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

Use SWA API endpoint to generate azmap token #458

Merged
merged 5 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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=
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,29 @@ First, copy `.env.sample` file to `.env`, and ensure the configuration values ar
|`REACT_APP_API_ROOT`| <https://planetarycomputer-staging.microsoft.com> | 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 <https://1dswhitelisting.azurewebsites.net/> | 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

Expand Down
15 changes: 12 additions & 3 deletions api/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 38 additions & 0 deletions api/map-token/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
19 changes: 19 additions & 0 deletions api/map-token/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
1 change: 1 addition & 0 deletions api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pc-datacatalog",
"version": "2024.1.0",
"version": "2024.1.2",
"private": true,
"proxy": "http://api:7071/",
"dependencies": {
Expand Down
38 changes: 38 additions & 0 deletions src/pages/Explore/components/Map/helpers.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<void> => {
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`;
Expand Down
8 changes: 5 additions & 3 deletions src/pages/Explore/components/Map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
});
Expand Down
6 changes: 6 additions & 0 deletions src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading