Skip to content

Commit

Permalink
Role definitions during deployment (#136)
Browse files Browse the repository at this point in the history
* Work on AAD roles deployment scripts

* Moved Set-FhirServerApiApplicationRoles

* Moved Set-FhirServerClientAppRoleAssignments to FhirServer module

* Created Set-FhirServerUserAppRoleAssignments.ps1 for adding users to roles

* Adjusted calls in FhirServerRelease to use new functions in FhirServer and updated deployment documentation

* Fixed bug and added instructions for specifying roles

* Check for role assignment when New-AzureADServiceAppRoleAssignment throws

* Fixed multiple incompatibilities in Add-AadTestAuthEnvironment.ps1, fixed a few bugs

* Updated documentation based on PR feedback

* Additional PR feedback
  • Loading branch information
hansenms authored Oct 25, 2018
1 parent ca52d9f commit 0c46787
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 77 deletions.
18 changes: 17 additions & 1 deletion docs/DefaultDeployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,29 @@ Register an AAD Application for the FHIR server API:

```PowerShell
$fhirServiceName = "myfhirservice"
$apiAppReg = New-FhirServerApiApplicationRegistration -FhirServiceName $fhirServiceName
$apiAppReg = New-FhirServerApiApplicationRegistration -FhirServiceName $fhirServiceName -AppRoles admin,nurse,patient
```

The `-AppRoles` defines a set of roles that can be granted to users or service principals (service accounts) interacting with the FHIR server API. Configuration settings for the FHIR server will determine which privileges (Read, Write, etc.) that are assosiated with each role.

To access the FHIR server from a client, you will also need a client AAD Application registration with a client secret. This client AAD Application registration will need to have appropriate application permissions and reply URLs configured. Here is how to register a client AAD Application for use with [Postman](https://getpostman.com):

```PowerShell
$clientAppReg = New-FhirServerClientApplicationRegistration -ApiAppId $apiAppReg.AppId -DisplayName "myfhirclient" -ReplyUrl "https://www.getpostman.com/oauth2/callback"
```

If you would like a client application to be able to act as a service account, you can assign roles to the client application:

```PowerShell
Set-FhirServerClientAppRoleAssignments -AppId $clientAppReg.AppId -ApiAppId $apiAppReg.AppId -AppRoles admin,patient
```

To assign roles to a specific user in Azure Active Directory:

```PowerShell
Set-FhirServerUserAppRoleAssignments -UserPrincipalName [email protected] -ApiAppId $apiAppReg.AppId -AppRoles admin,nurse
```

## Deploying the FHIR Server Template

To deploy the backend Cosmos DB, Azure Web App, and FHIR server code, use the buttons below to deploy through the Azure Portal. If you would like to protect the FHIR API with token authorization, you will need to supply application registration details as described above.
Expand Down Expand Up @@ -71,6 +85,8 @@ New-AzureRmResourceGroupDeployment `
-ResourceGroupName $rg.ResourceGroupName -serviceName $fhirServiceName
```

The default deployment will have a single role (`admin`) defined. To define more roles when deploying the server, see [details on specifying roles](Roles.md).

You can use [Postman to test the FHIR server](PostmanTesting.md).

## Clean up Azure AD App Registrations
Expand Down
96 changes: 96 additions & 0 deletions docs/Roles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Roles in Microsoft FHIR Server for Azure

The FHIR server uses a role based access control system. The privileges (Read, Write, etc.) assigned to specific roles are defined with an array structure:

```json
[
{
"name": "clinician",
"resourcePermissions": [
{
"actions": [
"Read",
"Write"
]
}
]
},
{
"name": "researcher",
"resourcePermissions": [
{
"actions": [
"Read"
]
}
]
},
{
"name": "admin",
"resourcePermissions": [
{
"actions": [
"Read",
"Write",
"HardDelete"
]
}
]
}
]
```

This structure is passed to the FHIR server at startup and enforced using the `roles` claim in the JWT access token presented when cusuming the FHIR server API.

When deploying the FHIR server into Azure using the provided [resource manager template](../samples/templates/default-azuredeploy.json) the array of roles can be passed in the `additionalFhirServerConfigProperties` parameter, which will add the roles to the [App Settings](https://docs.microsoft.com/en-us/azure/app-service/web-sites-configure) of the front end [Web App](https://azure.microsoft.com/en-us/services/app-service/web/) running the server.

In the app settings the nested array structure must be flattened and added to the `FhirServer:Security:Authorization:RoleConfiguration:Roles` section of the configuration. Specifically, a roles array like:

```json
[
{
"name": "admin",
"resourcePermissions": [
{
"actions": [
"Read",
"Write",
"HardDelete"
]
}
]
}
]
```

would become:

```json
{
"FhirServer:Security:Authorization:RoleConfiguration:Roles:0:name": "admin",
"FhirServer:Security:Authorization:RoleConfiguration:Roles:0:resourcePermissions:0:actions:0": "Read",
"FhirServer:Security:Authorization:RoleConfiguration:Roles:0:resourcePermissions:0:actions:1": "Write",
"FhirServer:Security:Authorization:RoleConfiguration:Roles:0:resourcePermissions:0:actions:2": "HardDelete"
}
```

To avoid having to maintain the role definitions in the less readable flattened form, you can use a script to convert the JSON form to the flattened table. There is a an example in the [ConvertTo-FlattenedConfigurationHashtable.ps1 script](../release/ConvertTo-FlattenedConfigurationHashtable.ps1). To use it:

```PowerShell
$roles = ConvertFrom-Json (Get-Content -Raw .\roles.json)
$flattenedRoles = .\release\scripts\PowerShell\ConvertTo-FlattenedConfigurationHashtable.ps1 -InputObject $roles -PathPrefix "FhirServer:Security:Authorization:RoleConfiguration:Roles"
```

To pass the array of roles in when deploying the FHIR server (see [Deployment Instructions](DefaultDeployment.md) for details):

```PowerShell
$rg = New-AzureRmResourceGroup -Name "RG-NAME" -Location westus2
New-AzureRmResourceGroupDeployment `
-TemplateUri "https://raw.githubusercontent.com/Microsoft/fhir-server/master/samples/templates/default-azuredeploy.json" `
-ResourceGroupName $rg.ResourceGroupName `
-serviceName $fhirServiceName `
-securityAuthenticationAuthority $apiAppReg.Authority `
-securityAuthenticationAudience $apiAppReg.Audience `
-additionalFhirServerConfigProperties $flattenedRoles
```
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ function Set-FhirServerApiUsers {
.DESCRIPTION
.PARAMETER TenantDomain
The domain of the AAD tenant.
.PARAMETER ServicePrincipalObjectId
The service principal for the AAD application that contains the roles to be assigned.
.PARAMETER ApiAppId
The AppId for the AAD application that contains the roles to be assigned.
.PARAMETER UserConfiguration
The collection of users from the testauthenvironment.json.
.PARAMETER UserNamePrefix
Expand All @@ -21,7 +21,7 @@ function Set-FhirServerApiUsers {

[Parameter(Mandatory = $true )]
[ValidateNotNullOrEmpty()]
[string]$ServicePrincipalObjectId,
[string]$ApiAppId,

[Parameter(Mandatory = $true )]
[ValidateNotNull()]
Expand Down Expand Up @@ -49,8 +49,6 @@ function Set-FhirServerApiUsers {

$environmentUsers = @()

$servicePrincipal = Get-AzureADServicePrincipal -ObjectId $ServicePrincipalObjectId

foreach ($user in $UserConfiguration) {
$userId = $user.id
if ($UserNamePrefix) {
Expand Down Expand Up @@ -86,35 +84,7 @@ function Set-FhirServerApiUsers {
id = $user.id
}

# Get the collection of roles for the user
$existingRoleAssignments = Get-AzureADUserAppRoleAssignment -ObjectId $aadUser.ObjectId | Where-Object {$_.ResourceId -eq $servicePrincipal.ObjectId}

$expectedRoles = New-Object System.Collections.ArrayList
$rolesToAdd = New-Object System.Collections.ArrayList
$rolesToRemove = New-Object System.Collections.ArrayList

foreach ($role in $user.roles) {
$expectedRoles += @($servicePrincipal.AppRoles | Where-Object { $_.DisplayName -eq $role })
}

foreach ($diff in Compare-Object -ReferenceObject @($expectedRoles | Select-Object) -DifferenceObject @($existingRoleAssignments | Select-Object) -Property "Id") {
switch ($diff.SideIndicator) {
"<=" {
$rolesToAdd += $diff.Id
}
"=>" {
$rolesToRemove += $diff.Id
}
}
}

foreach ($role in $rolesToAdd) {
New-AzureADUserAppRoleAssignment -ObjectId $aadUser.ObjectId -PrincipalId $aadUser.ObjectId -ResourceId $servicePrincipal.ObjectId -Id $role | Out-Null
}

foreach ($role in $rolesToRemove) {
Remove-AzureADUserAppRoleAssignment -ObjectId $aadUser.ObjectId -AppRoleAssignmentId ($existingRoleAssignments | Where-Object { $_.Id -eq $role }).ObjectId | Out-Null
}
Set-FhirServerUserAppRoleAssignments -ApiAppId $ApiAppId -UserPrincipalName $userUpn -AppRoles $user.roles
}

return $environmentUsers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ function Add-AadTestAuthEnvironment {
$retryCount = 0
# Make sure key vault exists and is ready
while (!(Get-AzureRmKeyVault -VaultName $keyVaultName )) {
$retryCout += 1
$retryCount += 1

if ($retry -gt 7) {
if ($retryCount -gt 7) {
throw "Could not connect to the vault $keyVaultName"
}

Expand Down Expand Up @@ -103,21 +103,20 @@ function Add-AadTestAuthEnvironment {
}

Write-Host "Setting roles on API Application"
Set-FhirServerApiApplicationRoles -ObjectId $application.ObjectId -RoleConfiguration $testAuthEnvironment.Roles | Out-Null

$servicePrincipal = Get-AzureAdServicePrincipalByAppId $application.AppId
$appRoles = ($testAuthEnvironment.roles | Select -ExpandProperty name)
Set-FhirServerApiApplicationRoles -ApiAppId $application.AppId -AppRoles $appRoles | Out-Null

Write-Host "Ensuring users and role assignments for API Application exist"
$environmentUsers = Set-FhirServerApiUsers -UserNamePrefix $EnvironmentName -TenantDomain $tenantInfo.TenantDomain -ServicePrincipalObjectId $servicePrincipal.ObjectId -UserConfiguration $testAuthEnvironment.Users -KeyVaultName $keyVaultName
$environmentUsers = Set-FhirServerApiUsers -UserNamePrefix $EnvironmentName -TenantDomain $tenantInfo.TenantDomain -ApiAppId $application.AppId -UserConfiguration $testAuthEnvironment.Users -KeyVaultName $keyVaultName

$environmentClientApplications = @()

Write-Host "Ensuring client application exists"
foreach ($clientApp in $testAuthEnvironment.ClientApplications) {
foreach ($clientApp in $testAuthEnvironment.clientApplications) {
$displayName = Get-ApplicationDisplayName -EnvironmentName $EnvironmentName -AppId $clientApp.Id
$aadClientApplication = Get-AzureAdApplicationByDisplayName $displayName

$publicClient = -not $clientApp.Roles
$publicClient = -not $clientApp.roles

if (!$aadClientApplication) {

Expand Down Expand Up @@ -145,9 +144,7 @@ function Add-AadTestAuthEnvironment {

Set-AzureKeyVaultSecret -VaultName $keyVaultName -Name "$displayName-secret" -SecretValue $secretSecureString | Out-Null

$aadClientServicePrincipal = Get-AzureAdServicePrincipalByAppId $aadClientApplication.AppId

Set-FhirServerClientAppRoleAssignments -ApiAppId $application.AppId -ObjectId $aadClientServicePrincipal.ObjectId -Roles $clientApp.Roles | Out-Null
Set-FhirServerClientAppRoleAssignments -ApiAppId $application.AppId -AppId $aadClientApplication.AppId -AppRoles $clientApp.roles | Out-Null
}

@{
Expand Down
2 changes: 1 addition & 1 deletion samples/scripts/PowerShell/FhirServer/FhirServer.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
CompanyName = 'https://microsoft.com'
Description = 'PowerShell Module for managing Azure Active Directory registrations and users for Microsoft FHIR Server.'
PowerShellVersion = '3.0'
FunctionsToExport = 'Remove-FhirServerApplicationRegistration', 'New-FhirServerClientApplicationRegistration', 'New-FhirServerApiApplicationRegistration'
FunctionsToExport = 'Remove-FhirServerApplicationRegistration', 'New-FhirServerClientApplicationRegistration', 'New-FhirServerApiApplicationRegistration', 'Get-FhirServerAzureAdAccessToken', 'Set-FhirServerApiApplicationRoles','Set-FhirServerClientAppRoleAssignments','Set-FhirServerUserAppRoleAssignments'
CmdletsToExport = @()
AliasesToExport = @()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ function New-FhirServerApiApplicationRegistration {
Create a new AAD Application registration for a FHIR server instance.
A FhirServiceName or FhirServiceAudience must be supplied.
.EXAMPLE
New-FhirServerApiApplicationRegistration -FhirServiceName "myfhiservice"
New-FhirServerApiApplicationRegistration -FhirServiceName "myfhiservice" -AppRoles admin,nurse
.EXAMPLE
New-FhirServerApiApplicationRegistration -FhirServiceAudience "https://myfhirservice.azurewebsites.net"
New-FhirServerApiApplicationRegistration -FhirServiceAudience "https://myfhirservice.azurewebsites.net" -AppRoles admin,nurse
.PARAMETER FhirServiceName
Name of the FHIR service instance.
.PARAMETER FhirServiceAudience
Full URL of the FHIR service.
.PARAMETER WebAppSuffix
Will be appended to FHIR service name to form the FhirServiceAudience if one is not supplied,
e.g., azurewebsites.net or azurewebsites.us (for US Government cloud)
.PARAMETER AppRoles
Names of AppRoles to be defined in the AAD Application registration
#>
[CmdletBinding(DefaultParameterSetName='ByFhirServiceName')]
param(
Expand All @@ -28,7 +30,10 @@ function New-FhirServerApiApplicationRegistration {
[string]$FhirServiceAudience,

[Parameter(Mandatory = $false, ParameterSetName = 'ByFhirServiceName' )]
[String]$WebAppSuffix = "azurewebsites.net"
[String]$WebAppSuffix = "azurewebsites.net",

[Parameter(Mandatory = $false)]
[String[]]$AppRoles = "admin"
)

Set-StrictMode -Version Latest
Expand All @@ -45,8 +50,22 @@ function New-FhirServerApiApplicationRegistration {
$FhirServiceAudience = "https://$FhirServiceName.$WebAppSuffix"
}

$desiredAppRoles = @()
foreach ($role in $AppRoles) {
$id = New-Guid

$desiredAppRoles += @{
AllowedMemberTypes = @("User", "Application")
Description = $role
DisplayName = $role
Id = $id
IsEnabled = "true"
Value = $role
}
}

# Create the App Registration
$apiAppReg = New-AzureADApplication -DisplayName $FhirServiceAudience -IdentifierUris $FhirServiceAudience
$apiAppReg = New-AzureADApplication -DisplayName $FhirServiceAudience -IdentifierUris $FhirServiceAudience -AppRoles $desiredAppRoles
New-AzureAdServicePrincipal -AppId $apiAppReg.AppId | Out-Null

$aadEndpoint = (Get-AzureADCurrentSessionInfo).Environment.Endpoints["ActiveDirectory"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
function Set-FhirServerApiApplicationRoles {
<#
.SYNOPSIS
Configures (create/update) the roles on the API application for the test environment.
Configures (create/update) the roles on the API application.
.DESCRIPTION
.PARAMETER ObjectId
ObjectId for the API application
.PARAMETER RoleConfiguration
Role configuration to be persisted to AAD from the testauthenvironment.json
Configures (create/update) the roles of the API Application registration, specifically, it populates the AppRoles field of the application manifest.
.EXAMPLE
Set-FhirServerApiApplicationRoles -AppId <ID of API App> -AppRoles admin,nurse,patient
.PARAMETER ApiAppId
ApiId for the API application
.PARAMETER AppRoles
List of roles to be defined on the API App
#>
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ObjectId,
[string]$ApiAppId,

[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[object]$RoleConfiguration
[string[]]$AppRoles
)

Set-StrictMode -Version Latest
Expand All @@ -29,15 +32,15 @@ function Set-FhirServerApiApplicationRoles {
}

Write-Host "Persisting Roles to AAD application"
$azureAdApplication = Get-AzureADApplication -ObjectId $ObjectId

$azureAdApplication = Get-AzureADApplication -Filter "AppId eq '$ApiAppId'"

$appRolesToDisable = $false
$appRolesToEnable = $false
$desiredAppRoles = @()

foreach ($role in $RoleConfiguration) {
$existingAppRole = $azureAdApplication.AppRoles | Where-Object Value -eq $role.name
foreach ($role in $AppRoles) {
$existingAppRole = $azureAdApplication.AppRoles | Where-Object Value -eq $role

if($existingAppRole) {
$id = $existingAppRole.Id
Expand All @@ -48,11 +51,11 @@ function Set-FhirServerApiApplicationRoles {

$desiredAppRoles += @{
AllowedMemberTypes = @("User", "Application")
Description = $role.name
DisplayName = $role.name
Description = $role
DisplayName = $role
Id = $id
IsEnabled = "true"
Value = $role.name
Value = $role
}
}

Expand Down
Loading

0 comments on commit 0c46787

Please sign in to comment.