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

Move to using managed identity for auth to CosmosDB. #3806

Merged
merged 35 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5071e45
Move to using managed identity for auth to CosmosDB.
marrobi Dec 6, 2023
81ceb1e
Add permissions to TRE DB to the API MSI
marrobi Dec 6, 2023
88b1f9b
Merge upstream
marrobi Dec 7, 2023
4ff3dfe
Update CHANGELOG
marrobi Dec 7, 2023
45a9fb7
Add missing await and fix tests.
marrobi Dec 20, 2023
3914d31
update core version
marrobi Dec 20, 2023
fc2dd9a
Update core version
marrobi Dec 20, 2023
5e7a417
remove DB create as this is done in terraform
marrobi Dec 20, 2023
97018a2
split management plane and data plane operations.
marrobi Dec 21, 2023
11f0f16
fix test
marrobi Dec 21, 2023
d0752e4
fix test
marrobi Dec 21, 2023
c345aa2
Refactor container creation.
marrobi Dec 21, 2023
0f6f592
remove import
marrobi Dec 21, 2023
f87f2bc
Added workaround for transport being closed.
marrobi Dec 21, 2023
69c8568
Move database connection to singleton
marrobi Dec 22, 2023
93729d3
fix linting
marrobi Dec 22, 2023
1fd7fa7
Remove async with cosmos_client as this is closing the client on exit
marrobi Dec 22, 2023
0a96818
Create method to get creds async without context manager
marrobi Dec 22, 2023
5715b1c
Merge branch 'main' into marrobi/issue345
marrobi Dec 22, 2023
6ab2fba
Factor out cosmos client from everywhere but base repo and health checks
marrobi Jan 2, 2024
8874a6a
Merge branch 'marrobi/issue345' of github.com:marrobi/AzureTRE into m…
marrobi Jan 2, 2024
7d85d04
Remove uneeded cosmos references
marrobi Jan 2, 2024
7ab298f
Remove uneeded database imports
marrobi Jan 2, 2024
b815846
fix async issue with FastAPI depends
marrobi Jan 2, 2024
96c296c
reduce changes
marrobi Jan 2, 2024
bfca0cd
reduce churn
marrobi Jan 2, 2024
4ff113e
Fix bad imports
marrobi Jan 2, 2024
8e98ebf
fix reverted change
marrobi Jan 2, 2024
df711f6
Move get container into database class
marrobi Jan 3, 2024
f82fd06
Remove await
marrobi Jan 3, 2024
7dfc153
tidy up credentials modifications
marrobi Jan 3, 2024
90f7557
fix linting
marrobi Jan 3, 2024
18b651d
fix msi handling
marrobi Jan 3, 2024
2e95926
missed file.
marrobi Jan 3, 2024
c33d9db
fix linting
marrobi Jan 3, 2024
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ FEATURES:

ENHANCEMENTS:
* Switch from OpenCensus to OpenTelemetry for logging ([#3762](https://github.com/microsoft/AzureTRE/pull/3762))
* Use managed identity for API connection to CosmosDB ([#345](https://github.com/microsoft/AzureTRE/issues/345))
* Switch to Structured Firewall Logs ([#3816](https://github.com/microsoft/AzureTRE/pull/3816))

BUG FIXES:
Expand Down
2 changes: 1 addition & 1 deletion api_app/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.17.1"
__version__ = "0.18.0"
2 changes: 1 addition & 1 deletion api_app/api/dependencies/airlock.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import Depends, HTTPException, Path, status
from pydantic import UUID4

from api.dependencies.database import get_repository
from api.helpers import get_repository
from db.repositories.airlock_requests import AirlockRequestRepository
from models.domain.airlock_request import AirlockRequest
from db.errors import EntityDoesNotExist, UnableToAccessDatabase
Expand Down
134 changes: 70 additions & 64 deletions api_app/api/dependencies/database.py
Original file line number Diff line number Diff line change
@@ -1,80 +1,86 @@
from typing import Callable, Type

from azure.cosmos.aio import CosmosClient
from azure.cosmos.aio import CosmosClient, DatabaseProxy, ContainerProxy
from azure.mgmt.cosmosdb.aio import CosmosDBManagementClient
from fastapi import Depends, FastAPI, HTTPException
from fastapi import Request, status
from core import config, credentials
from db.errors import UnableToAccessDatabase
from db.repositories.base import BaseRepository
from resources import strings

from core.config import MANAGED_IDENTITY_CLIENT_ID, STATE_STORE_ENDPOINT, STATE_STORE_KEY, STATE_STORE_SSL_VERIFY, SUBSCRIPTION_ID, RESOURCE_MANAGER_ENDPOINT, CREDENTIAL_SCOPES, RESOURCE_GROUP_NAME, COSMOSDB_ACCOUNT_NAME, STATE_STORE_DATABASE
from core.credentials import get_credential_async
from services.logging import logger


async def connect_to_db() -> CosmosClient:
logger.debug(f"Connecting to {config.STATE_STORE_ENDPOINT}")
class Singleton(type):
_instances = {}

def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]


try:
async with credentials.get_credential_async() as credential:
primary_master_key = await get_store_key(credential)
class Database(metaclass=Singleton):

if config.STATE_STORE_SSL_VERIFY:
_cosmos_client: CosmosClient = None
_database_proxy: DatabaseProxy = None

def __init__(cls):
pass

@classmethod
async def _connect_to_db(cls) -> CosmosClient:
logger.debug(f"Connecting to {STATE_STORE_ENDPOINT}")

credential = await get_credential_async()
if MANAGED_IDENTITY_CLIENT_ID:
logger.debug("Connecting with managed identity")
cosmos_client = CosmosClient(
url=config.STATE_STORE_ENDPOINT, credential=primary_master_key
url=STATE_STORE_ENDPOINT,
credential=credential
)
else:
# ignore TLS (setup is a pain) when using local Cosmos emulator.
cosmos_client = CosmosClient(
config.STATE_STORE_ENDPOINT, primary_master_key, connection_verify=False
)
logger.debug("Connecting with key")
primary_master_key = await cls._get_store_key(credential)

if STATE_STORE_SSL_VERIFY:
logger.debug("Connecting with SSL verification")
cosmos_client = CosmosClient(
url=STATE_STORE_ENDPOINT,
credential=primary_master_key
)
else:
logger.debug("Connecting without SSL verification")
# ignore TLS (setup is a pain) when using local Cosmos emulator.
cosmos_client = CosmosClient(
url=STATE_STORE_ENDPOINT,
credential=primary_master_key,
connection_verify=False
)
logger.debug("Connection established")
return cosmos_client
except Exception:
logger.exception("Connection to state store could not be established.")


async def get_store_key(credential) -> str:
if config.STATE_STORE_KEY:
primary_master_key = config.STATE_STORE_KEY
else:
async with CosmosDBManagementClient(
credential,
subscription_id=config.SUBSCRIPTION_ID,
base_url=config.RESOURCE_MANAGER_ENDPOINT,
credential_scopes=config.CREDENTIAL_SCOPES
) as cosmosdb_mng_client:
database_keys = await cosmosdb_mng_client.database_accounts.list_keys(
resource_group_name=config.RESOURCE_GROUP_NAME,
account_name=config.COSMOSDB_ACCOUNT_NAME,
)
primary_master_key = database_keys.primary_master_key

return primary_master_key


async def get_db_client(app: FastAPI) -> CosmosClient:
if not hasattr(app.state, 'cosmos_client') or not app.state.cosmos_client:
app.state.cosmos_client = await connect_to_db()
return app.state.cosmos_client

@classmethod
async def _get_store_key(cls, credential) -> str:
logger.debug("Getting store key")
if STATE_STORE_KEY:
primary_master_key = STATE_STORE_KEY
else:
async with CosmosDBManagementClient(
credential,
subscription_id=SUBSCRIPTION_ID,
base_url=RESOURCE_MANAGER_ENDPOINT,
credential_scopes=CREDENTIAL_SCOPES
) as cosmosdb_mng_client:
database_keys = await cosmosdb_mng_client.database_accounts.list_keys(
resource_group_name=RESOURCE_GROUP_NAME,
account_name=COSMOSDB_ACCOUNT_NAME,
)
primary_master_key = database_keys.primary_master_key

async def get_db_client_from_request(request: Request) -> CosmosClient:
return await get_db_client(request.app)
return primary_master_key

@classmethod
async def get_container_proxy(cls, container_name) -> ContainerProxy:
if cls._cosmos_client is None:
cls._cosmos_client = await cls._connect_to_db()

def get_repository(
repo_type: Type[BaseRepository],
) -> Callable[[CosmosClient], BaseRepository]:
async def _get_repo(
client: CosmosClient = Depends(get_db_client_from_request),
) -> BaseRepository:
try:
return await repo_type.create(client)
except UnableToAccessDatabase:
logger.exception(strings.STATE_STORE_ENDPOINT_NOT_RESPONDING)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=strings.STATE_STORE_ENDPOINT_NOT_RESPONDING,
)
if cls._database_proxy is None:
cls._database_proxy = cls._cosmos_client.get_database_client(STATE_STORE_DATABASE)

return _get_repo
return cls._database_proxy.get_container_client(container_name)
2 changes: 1 addition & 1 deletion api_app/api/dependencies/shared_services.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import Depends, HTTPException, Path, status
from pydantic import UUID4

from api.dependencies.database import get_repository
from api.helpers import get_repository
from db.errors import EntityDoesNotExist
from resources import strings
from models.domain.shared_service import SharedService
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/dependencies/workspace_service_templates.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import Depends, HTTPException, Path, status

from api.dependencies.database import get_repository
from api.helpers import get_repository
from db.errors import EntityDoesNotExist
from db.repositories.resource_templates import ResourceTemplateRepository
from models.domain.resource import ResourceType
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/dependencies/workspaces.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import Depends, HTTPException, Path, status
from pydantic import UUID4

from api.dependencies.database import get_repository
from api.helpers import get_repository
from db.errors import EntityDoesNotExist, ResourceIsNotDeployed
from db.repositories.operations import OperationRepository
from db.repositories.user_resources import UserResourceRepository
Expand Down
22 changes: 22 additions & 0 deletions api_app/api/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Callable, Type

from fastapi import HTTPException, status

from db.errors import UnableToAccessDatabase
from db.repositories.base import BaseRepository
from resources.strings import UNABLE_TO_GET_STATE_STORE_CLIENT
from services.logging import logger


def get_repository(repo_type: Type[BaseRepository],) -> Callable:
async def _get_repo() -> BaseRepository:
try:
return await repo_type.create()
except UnableToAccessDatabase:
logger.exception(UNABLE_TO_GET_STATE_STORE_CLIENT)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=UNABLE_TO_GET_STATE_STORE_CLIENT,
)

return _get_repo
2 changes: 1 addition & 1 deletion api_app/api/routes/airlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from fastapi import APIRouter, Depends, HTTPException, status as status_code, Response

from jsonschema.exceptions import ValidationError
from api.helpers import get_repository
from db.repositories.resources_history import ResourceHistoryRepository
from db.repositories.user_resources import UserResourceRepository
from db.repositories.workspace_services import WorkspaceServiceRepository
Expand All @@ -11,7 +12,6 @@
from db.repositories.airlock_requests import AirlockRequestRepository
from db.errors import EntityDoesNotExist, UserNotAuthorizedToUseTemplate

from api.dependencies.database import get_repository
from api.dependencies.workspaces import get_workspace_by_id_from_path, get_deployed_workspace_by_id_from_path
from api.dependencies.airlock import get_airlock_request_by_id_from_path
from models.domain.airlock_request import AirlockRequestStatus, AirlockRequestType
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from fastapi.openapi.docs import get_swagger_ui_html, get_swagger_ui_oauth2_redirect_html
from fastapi.openapi.utils import get_openapi

from api.dependencies.database import get_repository
from api.helpers import get_repository
from db.repositories.workspaces import WorkspaceRepository
from api.routes import health, ping, workspaces, workspace_templates, workspace_service_templates, user_resource_templates, \
shared_services, shared_service_templates, migrations, costs, airlock, operations, metadata
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from pydantic import UUID4

from models.schemas.costs import get_cost_report_responses, get_workspace_cost_report_responses
from api.dependencies.database import get_repository
from core import config
from api.helpers import get_repository
from db.repositories.shared_services import SharedServiceRepository
from db.repositories.user_resources import UserResourceRepository
from db.repositories.workspace_services import WorkspaceServiceRepository
Expand Down
8 changes: 4 additions & 4 deletions api_app/api/routes/health.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import asyncio
from fastapi import APIRouter
from fastapi import APIRouter, Request
from core import credentials
from models.schemas.status import HealthCheck, ServiceStatus, StatusEnum
from resources import strings
Expand All @@ -10,13 +10,13 @@


@router.get("/health", name=strings.API_GET_HEALTH_STATUS)
async def health_check() -> HealthCheck:
async def health_check(request: Request) -> HealthCheck:
# The health endpoint checks the status of key components of the system.
# Note that Resource Processor checks incur Azure management calls, so
# calling this endpoint frequently may result in API throttling.
async with credentials.get_credential_async() as credential:
async with credentials.get_credential_async_context() as credential:
cosmos, sb, rp = await asyncio.gather(
create_state_store_status(credential),
create_state_store_status(),
create_service_bus_status(credential),
create_resource_processor_status(credential)
)
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/migrations.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException, status
from db.migrations.airlock import AirlockMigration
from db.migrations.resources import ResourceMigration
from api.helpers import get_repository
from db.repositories.operations import OperationRepository
from db.repositories.resources_history import ResourceHistoryRepository
from services.authentication import get_current_admin_user
from resources import strings
from api.dependencies.database import get_repository
from db.migrations.shared_services import SharedServiceMigration
from db.migrations.workspaces import WorkspaceMigration
from db.repositories.resources import ResourceRepository
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/operations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends

from api.helpers import get_repository
from db.repositories.operations import OperationRepository
from api.dependencies.database import get_repository
from models.schemas.operation import OperationInList
from resources import strings
from services.authentication import get_current_tre_user_or_tre_admin
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/shared_service_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import parse_obj_as

from api.dependencies.database import get_repository
from api.helpers import get_repository
from db.errors import EntityDoesNotExist, EntityVersionExist, InvalidInput
from db.repositories.resource_templates import ResourceTemplateRepository
from models.domain.resource import ResourceType
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/shared_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from db.repositories.operations import OperationRepository
from db.errors import DuplicateEntity, MajorVersionUpdateDenied, UserNotAuthorizedToUseTemplate, TargetTemplateVersionDoesNotExist, VersionDowngradeDenied
from api.dependencies.database import get_repository
from api.helpers import get_repository
from api.dependencies.shared_services import get_shared_service_by_id_from_path, get_operation_by_id_from_path
from db.repositories.resource_templates import ResourceTemplateRepository
from db.repositories.resources_history import ResourceHistoryRepository
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/user_resource_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import parse_obj_as

from api.dependencies.database import get_repository
from api.dependencies.workspace_service_templates import get_workspace_service_template_by_name_from_path
from api.routes.resource_helpers import get_template
from db.errors import EntityVersionExist, InvalidInput
from api.helpers import get_repository
from db.repositories.resource_templates import ResourceTemplateRepository
from models.domain.resource import ResourceType
from models.schemas.user_resource_template import UserResourceTemplateInResponse, UserResourceTemplateInCreate
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/workspace_service_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import parse_obj_as

from api.dependencies.database import get_repository
from api.routes.resource_helpers import get_template
from db.errors import EntityVersionExist, InvalidInput
from api.helpers import get_repository
from db.repositories.resource_templates import ResourceTemplateRepository
from models.domain.resource import ResourceType
from models.schemas.resource_template import ResourceTemplateInResponse, ResourceTemplateInformationInList
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/workspace_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import parse_obj_as

from api.dependencies.database import get_repository
from api.helpers import get_repository
from db.errors import EntityVersionExist, InvalidInput
from db.repositories.resource_templates import ResourceTemplateRepository
from models.domain.resource import ResourceType
Expand Down
2 changes: 1 addition & 1 deletion api_app/api/routes/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from jsonschema.exceptions import ValidationError

from api.dependencies.database import get_repository
from api.helpers import get_repository
from api.dependencies.workspaces import get_operation_by_id_from_path, get_workspace_by_id_from_path, get_deployed_workspace_by_id_from_path, get_deployed_workspace_service_by_id_from_path, get_workspace_service_by_id_from_path, get_user_resource_by_id_from_path
from db.errors import InvalidInput, MajorVersionUpdateDenied, TargetTemplateVersionDoesNotExist, UserNotAuthorizedToUseTemplate, VersionDowngradeDenied
from db.repositories.operations import OperationRepository
Expand Down
Loading
Loading