Updating workflow collection to allow for use of API Token validation (#1141)

Updating workflow collection to allow for use of API Token validation

Signed-off-by: Ryan Lettieri <ryanLettieri@microsoft.com>
This commit is contained in:
Ryan Lettieri 2023-09-07 15:51:32 -06:00 committed by GitHub
parent fd7168fdfc
commit 87329f62b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 200 additions and 128 deletions

View File

@ -9,6 +9,10 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and
- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
- [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/)
## Optional Setup
Dapr workflow, as well as this example program, now support authentication through the use of API tokens. For more information on this, view the following document: [API Token](https://github.com/dapr/dotnet-sdk/docs/api-token.md)
## Projects in sample
This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project.

View File

@ -47,7 +47,16 @@ Console.ResetColor();
using var host = builder.Build();
host.Start();
using var daprClient = new DaprClientBuilder().Build();
DaprClient daprClient;
string apiToken = Environment.GetEnvironmentVariable("DAPR_API_TOKEN");
if (!string.IsNullOrEmpty(apiToken))
{
daprClient = new DaprClientBuilder().UseDaprApiToken(apiToken).Build();
}
else
{
daprClient = new DaprClientBuilder().Build();
}
// Wait for the sidecar to become available
while (!await daprClient.CheckHealthAsync())
@ -70,136 +79,138 @@ var baseInventory = new List<InventoryItem>
await RestockInventory(daprClient, baseInventory);
// Start the input loop
while (true)
using (daprClient)
{
// Get the name of the item to order and make sure we have inventory
string items = string.Join(", ", baseInventory.Select(i => i.Name));
Console.WriteLine($"Enter the name of one of the following items to order [{items}].");
Console.WriteLine("To restock items, type 'restock'.");
string itemName = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(itemName))
{
continue;
}
else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase))
{
await RestockInventory(daprClient, baseInventory);
continue;
}
InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase));
if (item == null)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"We don't have {itemName}!");
Console.ResetColor();
continue;
}
Console.WriteLine($"How many {itemName} would you like to purchase?");
string amountStr = Console.ReadLine().Trim();
if (!int.TryParse(amountStr, out int amount) || amount <= 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Invalid input. Assuming you meant to type '1'.");
Console.ResetColor();
amount = 1;
}
// Construct the order with a unique order ID
string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}";
double totalCost = amount * item.PerItemCost;
var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount);
// Start the workflow using the order ID as the workflow ID
Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}");
await daprClient.StartWorkflowAsync(
workflowComponent: DaprWorkflowComponent,
workflowName: nameof(OrderProcessingWorkflow),
input: orderInfo,
instanceId: orderId);
// Wait for the workflow to start and confirm the input
GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent);
Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs<OrderPayload>()}");
// Wait for the workflow to complete
while (true)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
// Get the name of the item to order and make sure we have inventory
string items = string.Join(", ", baseInventory.Select(i => i.Name));
Console.WriteLine($"Enter the name of one of the following items to order [{items}].");
Console.WriteLine("To restock items, type 'restock'.");
string itemName = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(itemName))
{
state = await daprClient.WaitForWorkflowCompletionAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent,
cancellationToken: cts.Token);
break;
continue;
}
catch (OperationCanceledException)
else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase))
{
// Check to see if the workflow is blocked waiting for an approval
state = await daprClient.GetWorkflowAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent);
if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) &&
customStatus.Contains("Waiting for approval"))
await RestockInventory(daprClient, baseInventory);
continue;
}
InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase));
if (item == null)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"We don't have {itemName}!");
Console.ResetColor();
continue;
}
Console.WriteLine($"How many {itemName} would you like to purchase?");
string amountStr = Console.ReadLine().Trim();
if (!int.TryParse(amountStr, out int amount) || amount <= 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Invalid input. Assuming you meant to type '1'.");
Console.ResetColor();
amount = 1;
}
// Construct the order with a unique order ID
string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}";
double totalCost = amount * item.PerItemCost;
var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount);
// Start the workflow using the order ID as the workflow ID
Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}");
await daprClient.StartWorkflowAsync(
workflowComponent: DaprWorkflowComponent,
workflowName: nameof(OrderProcessingWorkflow),
input: orderInfo,
instanceId: orderId);
// Wait for the workflow to start and confirm the input
GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent);
Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs<OrderPayload>()}");
// Wait for the workflow to complete
while (true)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]");
string approval = Console.ReadLine();
ApprovalResult approvalResult = ApprovalResult.Unspecified;
if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase))
state = await daprClient.WaitForWorkflowCompletionAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent,
cancellationToken: cts.Token);
break;
}
catch (OperationCanceledException)
{
// Check to see if the workflow is blocked waiting for an approval
state = await daprClient.GetWorkflowAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent);
if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) &&
customStatus.Contains("Waiting for approval"))
{
Console.WriteLine("Approving order...");
approvalResult = ApprovalResult.Approved;
}
else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Rejecting order...");
approvalResult = ApprovalResult.Rejected;
}
Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]");
string approval = Console.ReadLine();
ApprovalResult approvalResult = ApprovalResult.Unspecified;
if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Approving order...");
approvalResult = ApprovalResult.Approved;
}
else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Rejecting order...");
approvalResult = ApprovalResult.Rejected;
}
if (approvalResult != ApprovalResult.Unspecified)
{
// Raise the workflow event to the workflow
await daprClient.RaiseWorkflowEventAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent,
eventName: "ManagerApproval",
eventData: approvalResult);
}
if (approvalResult != ApprovalResult.Unspecified)
{
// Raise the workflow event to the workflow
await daprClient.RaiseWorkflowEventAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent,
eventName: "ManagerApproval",
eventData: approvalResult);
}
// otherwise, keep waiting
// otherwise, keep waiting
}
}
}
}
if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed)
{
OrderResult result = state.ReadOutputAs<OrderResult>();
if (result.Processed)
if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result}).");
OrderResult result = state.ReadOutputAs<OrderResult>();
if (result.Processed)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result}).");
Console.ResetColor();
}
else
{
Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed.");
}
}
else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"The workflow failed - {state.FailureDetails}");
Console.ResetColor();
}
else
{
Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed.");
}
}
else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"The workflow failed - {state.FailureDetails}");
Console.ResetColor();
}
Console.WriteLine();
Console.WriteLine();
}
}
static async Task RestockInventory(DaprClient daprClient, List<InventoryItem> inventory)
{
Console.WriteLine("*** Restocking inventory...");

View File

@ -17,6 +17,10 @@
<PackageReference Include="Microsoft.DurableTask.Worker.Grpc" Version="1.0.*" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Shared\DaprDefaults.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dapr.Client\Dapr.Client.csproj" />
<ProjectReference Include="..\Dapr.AspNetCore\Dapr.AspNetCore.csproj" />

View File

@ -14,16 +14,20 @@
namespace Dapr.Workflow
{
using System;
using Grpc.Net.Client;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Net.Http;
using Dapr;
/// <summary>
/// Contains extension methods for using Dapr Workflow with dependency injection.
/// </summary>
public static class WorkflowServiceCollectionExtensions
{
/// <summary>
/// Adds Dapr Workflow support to the service collection.
/// </summary>
@ -57,7 +61,18 @@ namespace Dapr.Workflow
if (TryGetGrpcAddress(out string address))
{
builder.UseGrpc(address);
var daprApiToken = DaprDefaults.GetDefaultDaprApiToken();
if (!string.IsNullOrEmpty(daprApiToken))
{
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken);
builder.UseGrpc(CreateChannel(address, client));
}
else
{
builder.UseGrpc(address);
}
}
else
{
@ -85,7 +100,18 @@ namespace Dapr.Workflow
{
if (TryGetGrpcAddress(out string address))
{
builder.UseGrpc(address);
var daprApiToken = DaprDefaults.GetDefaultDaprApiToken();
if (!string.IsNullOrEmpty(daprApiToken))
{
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken);
builder.UseGrpc(CreateChannel(address, client));
}
else
{
builder.UseGrpc(address);
}
}
else
{
@ -104,13 +130,13 @@ namespace Dapr.Workflow
// 1. DaprDefaults.cs uses 127.0.0.1 instead of localhost, which prevents testing with Dapr on WSL2 and the app on Windows
// 2. DaprDefaults.cs doesn't compile when the project has C# nullable reference types enabled.
// If the above issues are fixed (ensuring we don't regress anything) we should switch to using the logic in DaprDefaults.cs.
string? daprEndpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT");
var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint();
if (!String.IsNullOrEmpty(daprEndpoint)) {
address = daprEndpoint;
return true;
}
string? daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT");
var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT");
if (int.TryParse(daprPortStr, out int daprGrpcPort))
{
// There is a bug in the Durable Task SDK that requires us to change the format of the address
@ -126,6 +152,33 @@ namespace Dapr.Workflow
address = string.Empty;
return false;
}
static GrpcChannel CreateChannel(string address, HttpClient client)
{
GrpcChannelOptions options = new() { HttpClient = client};
var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint();
if (!String.IsNullOrEmpty(daprEndpoint)) {
return GrpcChannel.ForAddress(daprEndpoint, options);
}
var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT");
if (int.TryParse(daprPortStr, out int daprGrpcPort))
{
// If there is no address passed in, we default to localhost
if (String.IsNullOrEmpty(address))
{
// There is a bug in the Durable Task SDK that requires us to change the format of the address
// depending on the version of .NET that we're targeting. For now, we work around this manually.
#if NET6_0_OR_GREATER
address = $"http://localhost:{daprGrpcPort}";
#else
address = $"localhost:{daprGrpcPort}";
#endif
}
}
return GrpcChannel.ForAddress(address, options);
}
}
}

View File

@ -17,10 +17,10 @@ namespace Dapr
{
internal static class DaprDefaults
{
private static string httpEndpoint;
private static string grpcEndpoint;
private static string daprApiToken;
private static string appApiToken;
private static string httpEndpoint = string.Empty;
private static string grpcEndpoint = string.Empty;
private static string daprApiToken = string.Empty;
private static string appApiToken = string.Empty;
/// <summary>
/// Get the value of environment variable DAPR_API_TOKEN
@ -31,11 +31,11 @@ namespace Dapr
// Lazy-init is safe because this is just populating the default
// We don't plan to support the case where the user changes environment variables
// for a running process.
if (daprApiToken == null)
if (string.IsNullOrEmpty(daprApiToken))
{
// Treat empty the same as null since it's an environment variable
var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN");
daprApiToken = (value == string.Empty) ? null : value;
daprApiToken = string.IsNullOrEmpty(value) ? string.Empty : value;
}
return daprApiToken;
@ -47,10 +47,10 @@ namespace Dapr
/// <returns>The value of environment variable APP_API_TOKEN</returns>
public static string GetDefaultAppApiToken()
{
if (appApiToken == null)
if (string.IsNullOrEmpty(appApiToken))
{
var value = Environment.GetEnvironmentVariable("APP_API_TOKEN");
appApiToken = (value == string.Empty) ? null : value;
appApiToken = string.IsNullOrEmpty(value) ? string.Empty : value;
}
return appApiToken;
@ -62,7 +62,7 @@ namespace Dapr
/// <returns>The value of HTTP endpoint based off environment variables</returns>
public static string GetDefaultHttpEndpoint()
{
if (httpEndpoint == null)
if (string.IsNullOrEmpty(httpEndpoint))
{
var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT");
if (!string.IsNullOrEmpty(endpoint)) {
@ -84,7 +84,7 @@ namespace Dapr
/// <returns>The value of gRPC endpoint based off environment variables</returns>
public static string GetDefaultGrpcEndpoint()
{
if (grpcEndpoint == null)
if (string.IsNullOrEmpty(grpcEndpoint))
{
var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT");
if (!string.IsNullOrEmpty(endpoint)) {