-
Notifications
You must be signed in to change notification settings - Fork 849
Azure Storage Accounts PoLP (Principle of least privilege) Approach #2284
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
Changes from all commits
ea4b5e5
9d5c501
fa8a0d1
5665fdc
52f519c
a1c64a0
9b949cb
1a6324c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,21 +31,29 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context | |
| { | ||
| try | ||
| { | ||
| // Note: BlobServiceClient.GetPropertiesAsync() cannot be used with only the role assignment | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above, this comment should not be removed. |
||
| // "Storage Blob Data Contributor," so BlobServiceClient.GetBlobContainersAsync() is used instead to probe service health. | ||
| // However, BlobContainerClient.GetPropertiesAsync() does have sufficient permissions. | ||
| await _blobServiceClient | ||
| .GetBlobContainersAsync(cancellationToken: cancellationToken) | ||
| .AsPages(pageSizeHint: 1) | ||
| .GetAsyncEnumerator(cancellationToken) | ||
| .MoveNextAsync() | ||
| .ConfigureAwait(false); | ||
|
|
||
| if (!string.IsNullOrEmpty(_options.ContainerName)) | ||
| { | ||
| // Note: PoLP (Principle of least privilege) | ||
| // This can be used having at least the role assignment "Storage Blob Data Reader" at container level or at least "Storage Blob Data Reader" at storage account level. | ||
| // See https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app?tabs=dotnet#configure-permissions-for-access-to-blob-and-queue-data | ||
| var containerClient = _blobServiceClient.GetBlobContainerClient(_options.ContainerName); | ||
| await containerClient.GetPropertiesAsync(cancellationToken: cancellationToken).ConfigureAwait(false); | ||
| } | ||
| else | ||
| { | ||
| // Note: BlobServiceClient.GetPropertiesAsync() cannot be used with only the role assignment | ||
| // "Storage Blob Data Contributor," so BlobServiceClient.GetBlobContainersAsync() is used instead to probe service health. | ||
| // However, BlobContainerClient.GetPropertiesAsync() does have sufficient permissions. | ||
| // Note: PoLP (Principle of least privilege) | ||
| // This can be used having at least "Storage Blob Data Reader" at storage account level. | ||
| // See https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app?tabs=dotnet#configure-permissions-for-access-to-blob-and-queue-data | ||
| await _blobServiceClient | ||
| .GetBlobContainersAsync(cancellationToken: cancellationToken) | ||
| .AsPages(pageSizeHint: 1) | ||
| .GetAsyncEnumerator(cancellationToken) | ||
| .MoveNextAsync() | ||
| .ConfigureAwait(false); | ||
| } | ||
|
|
||
| return HealthCheckResult.Healthy(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,21 +31,29 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context | |
| { | ||
| try | ||
| { | ||
| // Note: QueueServiceClient.GetPropertiesAsync() cannot be used with only the role assignment | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same |
||
| // "Storage Queue Data Contributor," so QueueServiceClient.GetQueuesAsync() is used instead to probe service health. | ||
| // However, QueueClient.GetPropertiesAsync() does have sufficient permissions. | ||
| await _queueServiceClient | ||
| .GetQueuesAsync(cancellationToken: cancellationToken) | ||
| .AsPages(pageSizeHint: 1) | ||
| .GetAsyncEnumerator(cancellationToken) | ||
| .MoveNextAsync() | ||
| .ConfigureAwait(false); | ||
|
|
||
| if (!string.IsNullOrEmpty(_options.QueueName)) | ||
| { | ||
| // Note: PoLP (Principle of least privilege) | ||
| // This can be used having at least the role assignment "Storage Queue Data Reader" at container level or at least "Storage Queue Data Reader" at storage account level. | ||
| // See https://learn.microsoft.com/en-us/rest/api/storageservices/get-queue-metadata#authorization. | ||
| var queueClient = _queueServiceClient.GetQueueClient(_options.QueueName); | ||
| await queueClient.GetPropertiesAsync(cancellationToken).ConfigureAwait(false); | ||
| } | ||
| else | ||
| { | ||
| // Note: QueueServiceClient.GetPropertiesAsync() cannot be used with only the role assignment | ||
| // "Storage Queue Data Contributor," so QueueServiceClient.GetQueuesAsync() is used instead to probe service health. | ||
| // However, QueueClient.GetPropertiesAsync() does have sufficient permissions. | ||
| // Note: PoLP (Principle of least privilege) | ||
| // This can be used having at least "Storage Queue Data Reader" at storage account level. | ||
| // See https://learn.microsoft.com/en-us/rest/api/storageservices/get-queue-metadata#authorization. | ||
| await _queueServiceClient | ||
| .GetQueuesAsync(cancellationToken: cancellationToken) | ||
| .AsPages(pageSizeHint: 1) | ||
| .GetAsyncEnumerator(cancellationToken) | ||
| .MoveNextAsync() | ||
| .ConfigureAwait(false); | ||
| } | ||
|
|
||
| return HealthCheckResult.Healthy(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -55,36 +55,26 @@ public async Task return_healthy_when_only_checking_healthy_service() | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||||||||||||||||||||||
| public async Task return_healthy_when_checking_healthy_service_and_table() | ||||||||||||||||||||||||||||||||||||||||||
| public async Task return_healthy_when_checking_healthy_table() | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| using var tokenSource = new CancellationTokenSource(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableServiceClient | ||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note to other reviewers: we already have a test that is testing the code path where Lines 36 to 55 in f587a63
(this change looks like we test only 1 out of 2, but we keep testing both) |
||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync(filter: "false", cancellationToken: tokenSource.Token) | ||||||||||||||||||||||||||||||||||||||||||
| .Returns(AsyncPageable<TableItem>.FromPages([])); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableClient | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync<TableEntity>(filter: "false", cancellationToken: tokenSource.Token) | ||||||||||||||||||||||||||||||||||||||||||
| .Returns(AsyncPageable<TableEntity>.FromPages([])); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _options.TableName = TableName; | ||||||||||||||||||||||||||||||||||||||||||
| var actual = await _healthCheck.CheckHealthAsync(_context, tokenSource.Token); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableServiceClient | ||||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync(filter: "false", cancellationToken: tokenSource.Token); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableClient | ||||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync<TableEntity>(filter: "false", cancellationToken: tokenSource.Token); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| actual.Status.ShouldBe(HealthStatus.Healthy); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| [Theory] | ||||||||||||||||||||||||||||||||||||||||||
| [InlineData(false)] | ||||||||||||||||||||||||||||||||||||||||||
| [InlineData(true)] | ||||||||||||||||||||||||||||||||||||||||||
| public async Task return_unhealthy_when_checking_unhealthy_service(bool checkTable) | ||||||||||||||||||||||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||||||||||||||||||||||
| public async Task return_unhealthy_when_checking_unhealthy_service() | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| using var tokenSource = new CancellationTokenSource(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -103,7 +93,6 @@ public async Task return_unhealthy_when_checking_unhealthy_service(bool checkTab | |||||||||||||||||||||||||||||||||||||||||
| .MoveNextAsync() | ||||||||||||||||||||||||||||||||||||||||||
| .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.Unauthorized, "Unable to authorize access.")); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _options.TableName = checkTable ? TableName : null; | ||||||||||||||||||||||||||||||||||||||||||
| var actual = await _healthCheck.CheckHealthAsync(_context, tokenSource.Token); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableServiceClient | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -129,47 +118,21 @@ await enumerator | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||||||||||||||||||||||
| public async Task return_unhealthy_when_checking_unhealthy_container() | ||||||||||||||||||||||||||||||||||||||||||
| public async Task return_unhealthy_when_checking_unhealthy_table() | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| using var tokenSource = new CancellationTokenSource(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| var pageable = Substitute.For<AsyncPageable<TableEntity>>(); | ||||||||||||||||||||||||||||||||||||||||||
| var enumerator = Substitute.For<IAsyncEnumerator<TableEntity>>(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableServiceClient | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync(filter: "false", cancellationToken: tokenSource.Token) | ||||||||||||||||||||||||||||||||||||||||||
| .Returns(AsyncPageable<TableItem>.FromPages([])); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableClient | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync<TableEntity>(filter: "false", cancellationToken: tokenSource.Token) | ||||||||||||||||||||||||||||||||||||||||||
| .Returns(pageable); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| pageable | ||||||||||||||||||||||||||||||||||||||||||
| .GetAsyncEnumerator(tokenSource.Token) | ||||||||||||||||||||||||||||||||||||||||||
| .Returns(enumerator); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| enumerator | ||||||||||||||||||||||||||||||||||||||||||
| .MoveNextAsync() | ||||||||||||||||||||||||||||||||||||||||||
| .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.NotFound, "Table not found")); | ||||||||||||||||||||||||||||||||||||||||||
| .Throws(new RequestFailedException((int)HttpStatusCode.NotFound, "Table not found")); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _options.TableName = TableName; | ||||||||||||||||||||||||||||||||||||||||||
| var actual = await _healthCheck.CheckHealthAsync(_context, tokenSource.Token); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableServiceClient | ||||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync(filter: "false", cancellationToken: tokenSource.Token); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableClient | ||||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync<TableEntity>(filter: "false", cancellationToken: tokenSource.Token); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| pageable | ||||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .GetAsyncEnumerator(tokenSource.Token); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| await enumerator | ||||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .MoveNextAsync(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| actual.Status.ShouldBe(HealthStatus.Unhealthy); | ||||||||||||||||||||||||||||||||||||||||||
| actual | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -184,19 +147,15 @@ public async Task return_unhealthy_when_invoked_from_healthcheckservice() | |||||||||||||||||||||||||||||||||||||||||
| .AddSingleton(_tableServiceClient) | ||||||||||||||||||||||||||||||||||||||||||
| .AddLogging() | ||||||||||||||||||||||||||||||||||||||||||
| .AddHealthChecks() | ||||||||||||||||||||||||||||||||||||||||||
| .AddAzureTable(optionsFactory: _ => new AzureTableServiceHealthCheckOptions() { TableName = TableName }, name: HealthCheckName) | ||||||||||||||||||||||||||||||||||||||||||
| .AddAzureTable(optionsFactory: _ => new AzureTableServiceHealthCheckOptions(), name: HealthCheckName) | ||||||||||||||||||||||||||||||||||||||||||
| .Services | ||||||||||||||||||||||||||||||||||||||||||
| .BuildServiceProvider(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| var pageable = Substitute.For<AsyncPageable<TableEntity>>(); | ||||||||||||||||||||||||||||||||||||||||||
| var enumerator = Substitute.For<IAsyncEnumerator<TableEntity>>(); | ||||||||||||||||||||||||||||||||||||||||||
| var pageable = Substitute.For<AsyncPageable<TableItem>>(); | ||||||||||||||||||||||||||||||||||||||||||
| var enumerator = Substitute.For<IAsyncEnumerator<TableItem>>(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableServiceClient | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync(filter: "false", cancellationToken: Arg.Any<CancellationToken>()) | ||||||||||||||||||||||||||||||||||||||||||
| .Returns(AsyncPageable<TableItem>.FromPages([])); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableClient | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync<TableEntity>(filter: "false", cancellationToken: Arg.Any<CancellationToken>()) | ||||||||||||||||||||||||||||||||||||||||||
| .Returns(pageable); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| pageable | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -214,10 +173,6 @@ public async Task return_unhealthy_when_invoked_from_healthcheckservice() | |||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync(filter: "false", cancellationToken: Arg.Any<CancellationToken>()); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableClient | ||||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync<TableEntity>(filter: "false", cancellationToken: Arg.Any<CancellationToken>()); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| pageable | ||||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .GetAsyncEnumerator(Arg.Any<CancellationToken>()); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -230,4 +185,32 @@ await enumerator | |||||||||||||||||||||||||||||||||||||||||
| actual.Status.ShouldBe(HealthStatus.Unhealthy); | ||||||||||||||||||||||||||||||||||||||||||
| actual.Exception!.ShouldBeOfType<RequestFailedException>(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||||||||||||||||||||||
| public async Task return_unhealthy_when_invoked_from_healthcheckservice_for_table() | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| using var provider = new ServiceCollection() | ||||||||||||||||||||||||||||||||||||||||||
| .AddSingleton(_tableServiceClient) | ||||||||||||||||||||||||||||||||||||||||||
| .AddLogging() | ||||||||||||||||||||||||||||||||||||||||||
| .AddHealthChecks() | ||||||||||||||||||||||||||||||||||||||||||
| .AddAzureTable(optionsFactory: _ => new AzureTableServiceHealthCheckOptions() { TableName = TableName }, name: HealthCheckName) | ||||||||||||||||||||||||||||||||||||||||||
| .Services | ||||||||||||||||||||||||||||||||||||||||||
| .BuildServiceProvider(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableClient | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync<TableEntity>(filter: "false", cancellationToken: Arg.Any<CancellationToken>()) | ||||||||||||||||||||||||||||||||||||||||||
| .Throws(new RequestFailedException((int)HttpStatusCode.NotFound, "Table not found")); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| var service = provider.GetRequiredService<HealthCheckService>(); | ||||||||||||||||||||||||||||||||||||||||||
| var report = await service.CheckHealthAsync(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| _tableClient | ||||||||||||||||||||||||||||||||||||||||||
| .Received(1) | ||||||||||||||||||||||||||||||||||||||||||
| .QueryAsync<TableEntity>(filter: "false", cancellationToken: Arg.Any<CancellationToken>()); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| var actual = report.Entries[HealthCheckName]; | ||||||||||||||||||||||||||||||||||||||||||
| actual.Status.ShouldBe(HealthStatus.Unhealthy); | ||||||||||||||||||||||||||||||||||||||||||
| actual.Exception!.ShouldBeOfType<RequestFailedException>(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a valuable comment and it should be moved rather than deleted.