Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 15 additions & 9 deletions src/HealthChecks.Azure.Data.Tables/AzureTableServiceHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,30 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
try
{
// Note: TableServiceClient.GetPropertiesAsync() cannot be used with only the role assignment
// "Storage Table Data Contributor," so TableServiceClient.QueryAsync() and
Copy link
Collaborator

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.

// TableClient.QueryAsync<T>() are used instead to probe service health.
await _tableServiceClient
.QueryAsync(filter: "false", cancellationToken: cancellationToken)
.GetAsyncEnumerator(cancellationToken)
.MoveNextAsync()
.ConfigureAwait(false);

if (!string.IsNullOrEmpty(_options.TableName))
{
// Note: PoLP (Principle of least privilege)
// This can be used having at least the role assignment "Storage Table Data Reader" at table level.
var tableClient = _tableServiceClient.GetTableClient(_options.TableName);
await tableClient
.QueryAsync<TableEntity>(filter: "false", cancellationToken: cancellationToken)
.GetAsyncEnumerator(cancellationToken)
.MoveNextAsync()
.ConfigureAwait(false);
}
else
{
// Note: TableServiceClient.GetPropertiesAsync() cannot be used with only the role assignment
// "Storage Table Data Contributor," so TableServiceClient.QueryAsync() and
// TableClient.QueryAsync<T>() are used instead to probe service health.
// Note: PoLP (Principle of least privilege)
// This can can be used with only the role assignment "Storage Table Data Reader" at storage account level.
await _tableServiceClient
.QueryAsync(filter: "false", cancellationToken: cancellationToken)
.GetAsyncEnumerator(cancellationToken)
.MoveNextAsync()
.ConfigureAwait(false);
}

return HealthCheckResult.Healthy();
}
Expand Down
28 changes: 18 additions & 10 deletions src/HealthChecks.Azure.Storage.Blobs/AzureBlobStorageHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,29 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
try
{
// Note: BlobServiceClient.GetPropertiesAsync() cannot be used with only the role assignment
Copy link
Collaborator

Choose a reason for hiding this comment

The 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,29 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
try
{
// Note: QueueServiceClient.GetPropertiesAsync() cannot be used with only the role assignment
Copy link
Collaborator

Choose a reason for hiding this comment

The 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 _tableServiceClient is being used:

public async Task return_healthy_when_only_checking_healthy_service()
{
using var tokenSource = new CancellationTokenSource();
_tableServiceClient
.QueryAsync(filter: "false", cancellationToken: tokenSource.Token)
.Returns(AsyncPageable<TableItem>.FromPages(Array.Empty<Page<TableItem>>()));
var actual = await _healthCheck.CheckHealthAsync(_context, tokenSource.Token);
_tableServiceClient
.Received(1)
.QueryAsync(filter: "false", cancellationToken: tokenSource.Token);
_tableClient
.DidNotReceiveWithAnyArgs()
.QueryAsync<TableEntity>(default(string), default, default, default);
actual.Status.ShouldBe(HealthStatus.Healthy);
}

(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();

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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>());
Expand All @@ -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>();
}
}
Loading
Loading