Skip to content

Commit

Permalink
Use api endpoint to generate az maps token
Browse files Browse the repository at this point in the history
  • Loading branch information
mmcfarland committed Mar 6, 2024
1 parent c9656c3 commit f682088
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 17 deletions.
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.

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

0 comments on commit f682088

Please sign in to comment.