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

WIP Use Pulumi to create Entra applications #2248

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
3 changes: 2 additions & 1 deletion data_safe_haven/external/api/graph_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class GraphApi:
"Application.ReadWrite.All": "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9",
"AppRoleAssignment.ReadWrite.All": "06b708a9-e830-4db3-a914-8e69da51d44f",
"Directory.Read.All": "7ab1d382-f21e-4acd-a863-ba3e13f7da61",
"Directory.ReadWrite.All": "19dbc75e-c2e2-444c-a770-ec69d8559fc7",
"Domain.Read.All": "dbb9058a-0e50-45d7-ae91-66909b5d4664",
"Group.Read.All": "5b567255-7703-4780-807c-7be8301ae99b",
"Group.ReadWrite.All": "62a82d76-70ea-41e2-9197-370581804d09",
Expand Down Expand Up @@ -589,9 +590,9 @@ def grant_application_role_permissions(
f"Assigning application role '[green]{application_role_name}[/]' to '{application_name}'...",
)
request_json = {
"appRoleId": app_role_id,
"principalId": application_sp["id"],
"resourceId": microsoft_graph_sp["id"],
"appRoleId": app_role_id,
}
self.http_post(
f"{self.base_endpoint}/servicePrincipals/{microsoft_graph_sp['id']}/appRoleAssignments",
Expand Down
4 changes: 0 additions & 4 deletions data_safe_haven/infrastructure/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
from .dynamic import (
BlobContainerAcl,
BlobContainerAclProps,
EntraApplication,
EntraApplicationProps,
FileShareFile,
FileShareFileProps,
SSLCertificate,
Expand All @@ -28,8 +26,6 @@
__all__ = [
"BlobContainerAcl",
"BlobContainerAclProps",
"EntraApplication",
"EntraApplicationProps",
"FileShareFile",
"FileShareFileProps",
"LinuxVMComponentProps",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""Pulumi component for an Entra Application resource"""

from collections.abc import Mapping
from typing import Any

import pulumi_azuread as entra
from pulumi import ComponentResource, Input, Output, ResourceOptions

from data_safe_haven.functions import replace_separators


class EntraApplicationProps:
"""Properties for EntraApplicationComponent"""

def __init__(
self,
application_name: Input[str],
application_permissions: list[tuple[str, str]],
msgraph_service_principal: Input[entra.ServicePrincipal],
application_kwargs: Mapping[str, Any],
) -> None:
self.application_name = application_name
self.application_permissions = application_permissions
self.msgraph_client_id = msgraph_service_principal.client_id
self.msgraph_object_id = msgraph_service_principal.object_id
self.application_kwargs = application_kwargs

# Construct a mapping of all the available application permissions
self.msgraph_permissions: Output[dict[str, Mapping[str, str]]] = Output.all(
application=msgraph_service_principal.app_role_ids,
delegated=msgraph_service_principal.oauth2_permission_scope_ids,
).apply(
lambda kwargs: {
# 'Role' permissions belong to the application
"Role": kwargs["application"],
# 'Scope' permissions are delegated to users
"Scope": kwargs["delegated"],
}
)


class EntraDesktopApplicationProps(EntraApplicationProps):
"""
Properties for a desktop EntraApplicationComponent.
See https://learn.microsoft.com/en-us/entra/identity-platform/msal-client-applications)
"""

def __init__(
self,
application_name: Input[str],
application_permissions: list[tuple[str, str]],
msgraph_service_principal: Input[entra.ServicePrincipal],
):
super().__init__(
application_name=application_name,
application_kwargs={
"public_client": entra.ApplicationPublicClientArgs(
redirect_uris=["urn:ietf:wg:oauth:2.0:oob"]
)
},
application_permissions=application_permissions,
msgraph_service_principal=msgraph_service_principal,
)


class EntraWebApplicationProps(EntraApplicationProps):
"""
Properties for a web EntraApplicationComponent.
See https://learn.microsoft.com/en-us/entra/identity-platform/msal-client-applications)
"""

def __init__(
self,
application_name: Input[str],
application_permissions: list[tuple[str, str]],
msgraph_service_principal: Input[entra.ServicePrincipal],
redirect_url: Input[str],
):
super().__init__(
application_name=application_name,
application_kwargs={
"web": entra.ApplicationWebArgs(
redirect_uris=[redirect_url],
implicit_grant=entra.ApplicationWebImplicitGrantArgs(
id_token_issuance_enabled=True,
),
)
},
application_permissions=application_permissions,
msgraph_service_principal=msgraph_service_principal,
)


class EntraApplicationComponent(ComponentResource):
"""Deploy an Entra application with Pulumi"""

def __init__(
self,
name: str,
props: EntraApplicationProps,
opts: ResourceOptions | None = None,
) -> None:
super().__init__("dsh:common:EntraApplicationComponent", name, {}, opts)

# Create the application
self.application = entra.Application(
f"{self._name}_application",
display_name=props.application_name,
prevent_duplicate_names=True,
required_resource_accesses=(
[
entra.ApplicationRequiredResourceAccessArgs(
resource_accesses=[
entra.ApplicationRequiredResourceAccessResourceAccessArgs(
id=props.msgraph_permissions[scope][permission],
type=scope,
)
for scope, permission in props.application_permissions
],
resource_app_id=props.msgraph_client_id,
)
]
if props.application_permissions
else []
),
sign_in_audience="AzureADMyOrg",
**props.application_kwargs,
)

# Get the service principal for this application
self.application_service_principal = entra.ServicePrincipal(
f"{self._name}_application_service_principal",
client_id=self.application.client_id,
)

# Grant admin approval for requested application permissions
for scope, permission in props.application_permissions:
if scope == "application":
entra.AppRoleAssignment(
replace_separators(
f"{self._name}_application_grant_{scope}_{permission}".lower(),
"_",
),
app_role_id=props.msgraph_permissions[scope][permission],
principal_object_id=self.application_service_principal.object_id,
resource_object_id=props.msgraph_object_id,
)
if scope == "delegated":
entra.ServicePrincipalDelegatedPermissionGrant(
replace_separators(
f"{self._name}_application_delegated_grant_{scope}_{permission}".lower(),
"_",
),
claim_values=[permission],
resource_service_principal_object_id=props.msgraph_object_id,
service_principal_object_id=self.application_service_principal.object_id,
)
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
from .blob_container_acl import BlobContainerAcl, BlobContainerAclProps
from .entra_application import EntraApplication, EntraApplicationProps
from .file_share_file import FileShareFile, FileShareFileProps
from .ssl_certificate import SSLCertificate, SSLCertificateProps

__all__ = [
"BlobContainerAcl",
"BlobContainerAclProps",
"EntraApplication",
"EntraApplicationProps",
"FileShareFile",
"FileShareFileProps",
"SSLCertificate",
Expand Down
Loading
Loading