このエントリは2021/12/20現在の情報に基づいています。将来の機能追加や変更に伴い、記載内容との乖離が発生する可能性があります。
このエントリの目的
以前からManaged Identityを使ったCosmos DBへのアクセスは可能であったが、最近Azure ADのアカウントを使ってCosmos DBのデータ プレーン操作に対するRBACを構成できるようになった。
Azure Active Directory を使用して Azure Cosmos DB アカウントのロールベースのアクセス制御を構成する / Configure role-based access control with Azure Active Directory for your Azure Cosmos DB account
https://docs.microsoft.com/azure/cosmos-db/how-to-setup-rbac
以前のManaged Identityを使うやり方よりもきめ細かくRBACを構成でき、しかもCosmos DBのアクセスキーを取得する必要もないので堅牢性も高い。
【注意】権限モデルにも記載の通り、データ操作に対するRBACであり、管理リソースへのRBACではない。
上記ドキュメントでは、Service Principalを使ってRBACを構成するように見えるが、TokenCredential
はInterfaceであり、実装クラスとしてManagedIdentityCredential
があるので、Managed Identityでも可能であるはずで、それを試したのがこのエントリ。
結論から言うと、うまくいく。そして同様のしくみはApp Serviceでも利用可能である。
構成
今回は、Function appからCosmos DB Java SDK v4を使いCosmos DBにアクセスする。Function appのランタイムはJava 11とする(さすがにいまさらJava 8を使いたくないので)。Function appからはCosmos DBバインドを使うほうが簡単ではあるが、今回はHTTPバインドのFunction appでCosmos DBへのアクセスをするためにSDKを使うことにする(まったくもってお手盛り感しかない)。

準備
Function app
前述の通りFunctionの入出力はHTTP、中でCosmos DBのSDKを使ってCosmos DBへのクエリ、およびUpsertするようなFunction appを作成する。
Cosmos DBのSDKを使うにあたり、Credentialを取得する必要があるが、今回はManaged Identityを使うので、ドキュメントにある方法とは異なり、以下のようにManaged Identityのお作法でTokenCredential
を取得する必要がある。今回はDefaultAzureCredentialBuilder()
を使っているが、Managed Identityだけを対象にするなら、ManagedIdentityCredentialBuilder()
を使ってもいい。
TokenCredential tokenCredential = new DefaultAzureCredentialBuilder().build();
あとはCosmosClientもしくはCosmosAsyncClient作成時にtokenCredentialを渡すだけ。なので、キーなどを渡す必要はない。以下は同期Client作成の例。CosmosClientはAutoClosableなので、Try-with-Resourcesで囲む。
CosmosClient client = new CosmosClientBuilder()
.endpoint(ACCOUNT_ENDPOINT)
.credential(tokenCredential)
.gatewayMode()
.buildClient();
あとは、GETメソッドでの呼び出しなら取得、POSTメソッドでの呼び出し+HTTPリクエストにJSONドキュメントがあればUpsertするようなコードを作成する。Upsertのコードでは、Cosmos DB SDKからの応答コードをFunction appのHTTPステータスコードとして返すように構成しておく。
Track track = request.getBody().get();
CosmosItemResponse<Track> cosmosItemResponse = cosmosContainer.upsertItem(track);
HttpResponseMessage responseMessage = request.createResponseBuilder(HttpStatusType.custom(cosmosItemResponse.getStatusCode())).header("Content-Type", "application/json").body(track).build();
作成したらビルド、デプロイしておく。
続いてFunction appのManaged Identityを構成する。それぞれのFunction appで [設定] > [ID] > [システム割り当て済み]で状態をONにする。このとき現れるオブジェクトIDがPrincipal IDなので、メモしておく。以下はRead OnlyのFunction appの例。同様の設定をRead WriteのFunction appでも実施しておく。

Cosmos DB
まずはふつうにDatabase、Containerを作る。今回はDatabase名はInventories、Container名はTracksとした。Tracksに含まれるデータは以下のようなもの。
[
{
"id": "4321",
"type": "Hino",
"purchaseDate": "2020/10/20",
"remarks": "4t"
},
{
"id": "1234",
"type": "Isuzu",
"purchaseDate": "2020/11/20",
"remarks": "4t"
},
{
"id": "6666",
"type": "Fuso",
"purchaseDate": "2020/12/20",
"remarks": "4t"
},
{
"id": "1111",
"type": "Volvo",
"purchaseDate": "2021/09/20",
"remarks": "4t"
},
{
"id": "1112",
"type": "Volvo",
"purchaseDate": "2021/09/20",
"remarks": "4t"
},
{
"id": "1216",
"type": "Volvo",
"purchaseDate": "2021/09/20",
"remarks": "4t"
}
]
続いて、カスタムロールを作成する。一つは読み取りのみを許可するロール、もう一つは読み取り・書き込みを許可するロール。今回はAzure CLIで作成する。この例はドキュメントの記述のまんま。
以下は読み取りのみ可能なロール作成のためのJSONファイル(role-definition-ro.json
)。
{
"RoleName": "MyReadOnlyRole",
"Type": "CustomRole",
"AssignableScopes": ["/"],
"Permissions": [{
"DataActions": [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed"
]
}]
}
以下は読み書き可能なロール作成のためのJSONファイル(role-definition-rw.json
)。
{
"RoleName": "MyReadWriteRole",
"Type": "CustomRole",
"AssignableScopes": ["/"],
"Permissions": [{
"DataActions": [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*"
]
}]
}
先ほど作成したJSONファイルでカスタムロールを作成する。以下はAzure CLIの例。
resourceGroupName='<myResourceGroup>'
accountName='<myCosmosAccount>'
az cosmosdb sql role definition create -a $accountName -g $resourceGroupName -b @role-definition-ro.json
az cosmosdb sql role definition create -a $accountName -g $resourceGroupName -b @role-definition-rw.json
ここまでで、MyReadOnlyRoleとMyReadWriteRoleの作成が完了。以後の作業のため、roleDefinitionIdを取得する。以下のコマンドを実行して、出力されるJSONのnameがroleDefinitionId。
az cosmosdb sql role definition list --account-name $accountName -g $resourceGroupName
実行例は以下。
[
{
"assignableScopes": [
"/subscriptions/{subscription id}/resourceGroups/{resource group}/providers/Microsoft.DocumentDB/databaseAccounts/{CosmosDB account}"
],
"id": "/subscriptions/{subscription id}/resourceGroups/{resource group}/providers/Microsoft.DocumentDB/databaseAccounts/{CosmosDB account}/sqlRoleDefinitions/{roleDefinitionId}",
"name": "{roleDefinitionId}",
"permissions": [
{
"dataActions": [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*"
],
"notDataActions": []
}
],
"resourceGroup": "{resource group}",
"roleName": "MyReadWriteRole",
"type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions",
"typePropertiesType": "1"
},
{
"assignableScopes": [
"/subscriptions/{subscription id}/resourceGroups/{resource group}/providers/Microsoft.DocumentDB/databaseAccounts/{CosmosDB account}"
],
"id": "/subscriptions/{subscription id}/resourceGroups/{resource group}/providers/Microsoft.DocumentDB/databaseAccounts/{CosmosDB account}/sqlRoleDefinitions/{roleDefinitionId}",
"name": "{roleDefinitionId}",
"permissions": [
{
"dataActions": [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed"
],
"notDataActions": []
}
],
"resourceGroup": "{resource group}",
"roleName": "MyReadOnlyRole",
"type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions",
"typePropertiesType": "1"
},
...
]
MyReadOnlyRoleとMyReadWriteRoleに対応するRoleDefinitionIdを使ってロールに参加させる。Principal IDはAzure Active Directory portal ブレードの [エンタープライズ アプリケーション] セクションにある [オブジェクト ID] であり、これはManaged Identityでも同じように設定すればよい。
resourceGroupName='<myResourceGroup>'
accountName='<myCosmosAccount>'
readOnlyRoleDefinitionId = '<MyReadOnlyRoleのroleDefinitionId>'
ROprincipalId = '<ReadOnly用に作成したFunction AppのManaged IdentityのObject ID>'
az cosmosdb sql role assignment create -a $accountName -g $resourceGroupName -s "/" -p $ROprincipalId -d $readOnlyRoleDefinitionId
readWriteRoleDefinitionId = '<MyReadWriteRoleのroleDefinitionId>'
RWprincipalId = '<ReadWrite用に作成したFunction AppのManaged IdentityのObject ID>'
az cosmosdb sql role assignment create -a $accountName -g $resourceGroupName -s "/" -p $RWprincipalId -d $readWriteRoleDefinitionId
これでおしまい。
テスト
では、Read OnlyのFunction appから試す。GETメソッドでは問題なく取得できる。

POSTで更新もしくは追加やDELETEの場合は、403が返る。マスクした部分はRead OnlyのFunction Appで作成したManaged IdentityのPrincipal IDに一致する。以下はPOSTの例。

Azure Portalのログストリームからも確認できる。

対して、Read Write可能なFunction appの場合は、GET、POSTとも動作する。

POSTで書き換えて…

再度GETすると、当然ながらid: 4321のtypeが更新されている。

まとめ
Cosmos DBへアクセスするアプリケーションに対してRBACを構成しておけば、読み取り専用アプリなのに間違って更新ロジックが入っていた場合でも、RBACのおかげで無用な事故がなくなるというメリットを享受できる(もっとも、読み取り専用なのに更新ロジックが入っているのはテストできていないんじゃないか、とか、単にコピペしたんではないか、と思うのは尤もな話)。
おまけ
このデモのソースコードは以下にある。
CosmosDB_RBAC_Demo
https://github.com/anishi1222/CosmosDB_RBAC_Demo