MNT: Upgrade to password-free authentication (#925)

Remove the use of Service Principal authentication.
To make this work, I also had to add to modify the Service Principal. In
the "Clients & Secrets" section, add "Federated Credentials". Choose
"Scenario: GitHub Action". Fill in repo details, "Entity Type: Pull
Request". Name does not matter.
This commit is contained in:
Anton Schwaighofer 2024-04-12 13:26:41 +01:00 коммит произвёл GitHub
Родитель 61a2c4d330
Коммит 9ec8fd4426
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
10 изменённых файлов: 164 добавлений и 24 удалений

45
.github/actions/azure-login/action.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,45 @@
name: 'Log into Azure using OpenID Connect'
runs:
using: "composite"
steps:
# Github agents runners sometimes are out of sync with the NTP server, and this can cause issues with the token expiry.
# Hence, sync time before logging into Azure.
- name: Synchronize time with NTP server
run: sudo timedatectl set-ntp true
shell: bash
- name: Login to Azure via OpenID Connect
uses: azure/login@v1
with:
client-id: ${{ env.HIML_SERVICE_PRINCIPAL_ID }}
tenant-id: ${{ env.HIML_TENANT_ID }}
subscription-id: ${{ env.HIML_SUBSCRIPTION_ID }}
# The step above only acquires an ID token and an access token for the Azure Resource Manager scope.
# This ID token has an expiry of 10min. During the tests, we will acquire access tokens for further
# scopes, but at that time, the ID token will be expired. Hence, acquire those access tokens now,
# because they will have an expiry of 60min.
# https://github.com/Azure/azure-cli/issues/28708#issuecomment-2047256166
- name: Get access tokens
run: |
az account get-access-token --scope https://management.azure.com/.default --output none
az account get-access-token --scope https://storage.azure.com/.default --output none
az account get-access-token --scope https://ml.azure.com/.default --output none
az account get-access-token --scope https://management.core.windows.net/.default --output none
shell: bash
# Workaround for bug in MSAL taken from
# https://github.com/Azure/azure-cli/issues/28708#issuecomment-2049718869
- name: Fetch OID token every 4 mins in the background
shell: bash
run: |
while true; do
token_request=$ACTIONS_ID_TOKEN_REQUEST_TOKEN
token_uri=$ACTIONS_ID_TOKEN_REQUEST_URL
token=$(curl -H "Authorization: bearer $token_request" "${token_uri}&audience=api://AzureADTokenExchange" | jq .value -r)
az login --service-principal -u ${{ env.HIML_SERVICE_PRINCIPAL_ID }} -t ${{ env.HIML_TENANT_ID }} --federated-token $token --output none
# Sleep for 4 minutes
sleep 240
done &

Просмотреть файл

@ -2,7 +2,8 @@ name: AzureML_SDK
channels:
- defaults
dependencies:
- pip=23.3
- python=3.9.18
- pip=23.0.1
- python=3.10
- pip:
- azureml-sdk==1.53.0
- azureml-sdk==1.54.0
- azure-cli

Просмотреть файл

@ -7,16 +7,12 @@ import re
from azureml._restclient.constants import RunStatus
from azureml.core import Experiment, Run, Workspace
from azureml.core.authentication import ServicePrincipalAuthentication
from azureml.core.authentication import AzureCliAuthentication
def cancel_running_and_queued_jobs() -> None:
print("Authenticating")
auth = ServicePrincipalAuthentication(
tenant_id='72f988bf-86f1-41af-91ab-2d7cd011db47',
service_principal_id=os.environ["HIML_SERVICE_PRINCIPAL_ID"],
service_principal_password=os.environ["HIML_SERVICE_PRINCIPAL_PASSWORD"],
)
auth = AzureCliAuthentication()
print("Getting AML workspace")
workspace = Workspace.get(
name="hi-ml",

52
.github/workflows/cpath-pr.yml поставляемый
Просмотреть файл

@ -18,6 +18,12 @@ concurrency:
group: ${{ github.ref }}/cpath-pr
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
permissions:
# This is required for requesting the Azure login token
id-token: write
# This is required for actions/checkout
contents: read
env:
pythonVersion: 3.9
folder: hi-ml-cpath
@ -26,7 +32,6 @@ env:
HIML_SUBSCRIPTION_ID: ${{ secrets.HIML_SUBSCRIPTION_ID }}
HIML_WORKSPACE_NAME: ${{ secrets.HIML_WORKSPACE_NAME }}
HIML_SERVICE_PRINCIPAL_ID: ${{ secrets.HIML_SERVICE_PRINCIPAL_ID }}
HIML_SERVICE_PRINCIPAL_PASSWORD: ${{ secrets.HIML_SERVICE_PRINCIPAL_PASSWORD }}
# Set the AML experiment name for all AML jobs submitted during tests. Github.ref looks like
# "refs/pull/123/merge" for PR builds.
HIML_EXPERIMENT_NAME: ${{ github.ref }}
@ -38,6 +43,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Cancel previous AzureML runs
uses: ./.github/actions/cancel_azureml_jobs
@ -87,6 +95,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Test with pytest
run: |
cd ${{ env.folder }}
@ -114,6 +125,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Run GPU tests
run: |
cd ${{ env.folder }}
@ -138,6 +152,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -154,6 +171,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -170,6 +190,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -186,6 +209,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -202,6 +228,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -218,6 +247,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -234,6 +266,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -250,6 +285,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -266,6 +304,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -282,6 +323,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -298,6 +342,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}
@ -314,6 +361,9 @@ jobs:
- name: Prepare Conda environment
uses: ./.github/actions/prepare_cpath_environment
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: smoke test
run: |
cd ${{ env.folder }}

31
.github/workflows/hi-ml-pr.yml поставляемый
Просмотреть файл

@ -16,6 +16,12 @@ concurrency:
group: ${{ github.ref }}/hi-ml-pr
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
permissions:
# This is required for requesting the Azure login token
id-token: write
# This is required for actions/checkout
contents: read
env:
pythonVersion: 3.9
HIML_TENANT_ID: ${{ secrets.HIML_TENANT_ID }}
@ -23,7 +29,6 @@ env:
HIML_SUBSCRIPTION_ID: ${{ secrets.HIML_SUBSCRIPTION_ID }}
HIML_WORKSPACE_NAME: ${{ secrets.HIML_WORKSPACE_NAME }}
HIML_SERVICE_PRINCIPAL_ID: ${{ secrets.HIML_SERVICE_PRINCIPAL_ID }}
HIML_SERVICE_PRINCIPAL_PASSWORD: ${{ secrets.HIML_SERVICE_PRINCIPAL_PASSWORD }}
HIML_DIST_ARTIFACT_SUFFIX: '-dist'
HIML_PACKAGE_NAME_ARTIFACT_SUFFIX: '-package_name'
HIML_VERSION_ARTIFACT_SUFFIX: '-latest_version'
@ -40,6 +45,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Cancel previous AzureML runs
uses: ./.github/actions/cancel_azureml_jobs
@ -126,6 +134,9 @@ jobs:
# Install local package in editable mode
make pip_local
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Test with pytest, fast only
run: |
cd ${{ matrix.folder }}
@ -202,6 +213,9 @@ jobs:
with:
folder: ${{ matrix.folder }}
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Install artifact and test
run: |
cd ${{ matrix.folder }}
@ -237,6 +251,9 @@ jobs:
with:
python-version: ${{ env.pythonVersion }}
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Run smoke_helloworld_v1
run: |
cd hi-ml
@ -255,6 +272,9 @@ jobs:
with:
python-version: ${{ env.pythonVersion }}
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Run smoke_helloworld_v2
run: |
cd hi-ml
@ -273,6 +293,9 @@ jobs:
with:
python-version: ${{ env.pythonVersion }}
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Run smoke_helloworld_v1_2nodes
run: |
cd hi-ml
@ -291,6 +314,9 @@ jobs:
with:
python-version: ${{ env.pythonVersion }}
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Run smoke_helloworld_v2_2nodes
run: |
cd hi-ml
@ -398,6 +424,9 @@ jobs:
# Install local package in editable mode
make pip_local
- name: Azure login using OpenID Connect
uses: ./.github/actions/azure-login
- name: Install PyPI package and test
run: |
cd ${{ matrix.folder }}

1
.github/workflows/multimodal-pr.yml поставляемый
Просмотреть файл

@ -20,7 +20,6 @@ env:
HIML_SUBSCRIPTION_ID: ${{ secrets.HIML_SUBSCRIPTION_ID }}
HIML_WORKSPACE_NAME: ${{ secrets.HIML_WORKSPACE_NAME }}
HIML_SERVICE_PRINCIPAL_ID: ${{ secrets.HIML_SERVICE_PRINCIPAL_ID }}
HIML_SERVICE_PRINCIPAL_PASSWORD: ${{ secrets.HIML_SERVICE_PRINCIPAL_PASSWORD }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs:

Просмотреть файл

@ -25,6 +25,12 @@ ENV_SERVICE_PRINCIPAL_ID = "HIML_SERVICE_PRINCIPAL_ID"
ENV_SERVICE_PRINCIPAL_PASSWORD = "HIML_SERVICE_PRINCIPAL_PASSWORD"
ENV_TENANT_ID = "HIML_TENANT_ID"
# This is an environment variable that is set by GitHub Actions, for checking if the code is running in GitHub
ENV_GITHUB_ACTIONS = "GITHUB_ACTIONS"
# The scope for the access tokens that are requested from Azure
ACCESS_TOKEN_SCOPE = "https://management.azure.com/.default"
def get_secret_from_environment(name: str, allow_missing: bool = False) -> Optional[str]:
"""
@ -69,11 +75,17 @@ def get_authentication() -> (
try:
logger.debug("Trying to authenticate using Azure CLI")
auth = AzureCliAuthentication()
_ = auth.get_token()
_ = auth.get_token(ACCESS_TOKEN_SCOPE)
logger.info("Successfully started AzureCLI authentication.")
return auth
except AuthenticationException:
pass
except AuthenticationException as ex:
# If the code is running in GitHub, there is no point in even trying to authenticate interactively.
# Raise the exception to get some information about the authentication problem.
# Otherwise, try to authenticate interactively.
# The GITHUB_ACTIONS environment variable is meant to be used exactly for this check
# https://docs.github.com/en/actions/learn-github-actions/variables
if os.getenv(ENV_GITHUB_ACTIONS, "") == "true":
raise AuthenticationException("AzureCLI authentication must be set up when running in GitHub") from ex
logger.info(
"Using interactive login to Azure. To use Service Principal authentication, set the environment "
@ -90,7 +102,7 @@ def _validate_credential(credential: TokenCredential) -> None:
:param credential: The credential object to validate.
"""
credential.get_token("https://management.azure.com/.default")
credential.get_token(ACCESS_TOKEN_SCOPE)
def _get_legitimate_service_principal_credential(

Просмотреть файл

@ -146,15 +146,19 @@ def test_get_legitimate_device_code_credential() -> None:
@pytest.mark.fast
def test_get_legitimate_default_credential() -> None:
@pytest.mark.skip(reason="Default azure credential are now the default in CI, and test hence fails")
def test_get_legitimate_default_credential_fails() -> None:
def _mock_credential_fast_timeout(timeout: int) -> DefaultAzureCredential:
return DefaultAzureCredential(timeout=1)
with patch("health_azure.auth.DefaultAzureCredential", new=_mock_credential_fast_timeout):
exception_message = r"DefaultAzureCredential failed to retrieve a token from the included credentials."
with pytest.raises(ClientAuthenticationError, match=exception_message):
cred = _get_legitimate_default_credential()
_get_legitimate_default_credential()
@pytest.mark.fast
def test_get_legitimate_default_credential() -> None:
with patch("health_azure.auth._validate_credential"):
cred = _get_legitimate_default_credential()
assert isinstance(cred, DefaultAzureCredential)

Просмотреть файл

@ -10,7 +10,6 @@ from pathlib import Path
from uuid import uuid4
from azureml.core.authentication import (
InteractiveLoginAuthentication,
ServicePrincipalAuthentication,
AzureCliAuthentication,
)
@ -25,6 +24,7 @@ from unittest.mock import MagicMock, patch
from health_azure.auth import (
get_secret_from_environment,
get_authentication,
ENV_GITHUB_ACTIONS,
ENV_SERVICE_PRINCIPAL_ID,
ENV_SERVICE_PRINCIPAL_PASSWORD,
ENV_TENANT_ID,
@ -104,10 +104,12 @@ def test_find_file_in_parent_folders(caplog: LogCaptureFixture) -> None:
@pytest.mark.fast
@patch("health_azure.auth.InteractiveLoginAuthentication")
def test_get_authentication(mock_interactive_authentication: MagicMock) -> None:
with patch.dict(os.environ, {}, clear=True):
get_authentication()
assert mock_interactive_authentication.called
def test_get_authentication(mock_auth: MagicMock) -> None:
# Disable Azure CLI authentication and SP via mocks
with patch("health_azure.auth.AzureCliAuthentication", side_effect=AuthenticationException("")):
with patch.dict(os.environ, {}, clear=True):
get_authentication()
assert mock_auth.called, "Expected InteractiveLoginAuthentication to be called"
service_principal_id = "1"
tenant_id = "2"
service_principal_password = "3"
@ -228,6 +230,7 @@ def test_auth_azure_cli() -> None:
ENV_SERVICE_PRINCIPAL_ID: "foo",
ENV_TENANT_ID: "bar",
ENV_SERVICE_PRINCIPAL_PASSWORD: "",
ENV_GITHUB_ACTIONS: "",
}
# Patch environment variables to have no service principal

Просмотреть файл

@ -7,6 +7,7 @@ from __future__ import annotations
import tempfile
from pathlib import Path
from typing import Any
from torch.hub import load_state_dict_from_url
from torchvision.datasets.utils import download_url
@ -75,7 +76,7 @@ def get_biovil_image_encoder(pretrained: bool = True) -> ImageModel:
return image_model
def get_biovil_t_image_encoder(**kwargs) -> ImageModel:
def get_biovil_t_image_encoder(**kwargs: Any) -> ImageModel:
"""Download weights from Hugging Face and instantiate the image model."""
biovilt_checkpoint_path = _download_biovil_t_image_model_weights()