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/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
- [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) - [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 ## Projects in sample
This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project. This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project.

View File

@ -47,7 +47,16 @@ Console.ResetColor();
using var host = builder.Build(); using var host = builder.Build();
host.Start(); 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 // Wait for the sidecar to become available
while (!await daprClient.CheckHealthAsync()) while (!await daprClient.CheckHealthAsync())
@ -70,136 +79,138 @@ var baseInventory = new List<InventoryItem>
await RestockInventory(daprClient, baseInventory); await RestockInventory(daprClient, baseInventory);
// Start the input loop // 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) while (true)
{ {
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Get the name of the item to order and make sure we have inventory
try 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( continue;
instanceId: orderId,
workflowComponent: DaprWorkflowComponent,
cancellationToken: cts.Token);
break;
} }
catch (OperationCanceledException) else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase))
{ {
// Check to see if the workflow is blocked waiting for an approval await RestockInventory(daprClient, baseInventory);
state = await daprClient.GetWorkflowAsync( continue;
instanceId: orderId, }
workflowComponent: DaprWorkflowComponent);
if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) && InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase));
customStatus.Contains("Waiting for approval")) 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]"); state = await daprClient.WaitForWorkflowCompletionAsync(
string approval = Console.ReadLine(); instanceId: orderId,
ApprovalResult approvalResult = ApprovalResult.Unspecified; workflowComponent: DaprWorkflowComponent,
if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase)) 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..."); Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]");
approvalResult = ApprovalResult.Approved; string approval = Console.ReadLine();
} ApprovalResult approvalResult = ApprovalResult.Unspecified;
else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase)) if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase))
{ {
Console.WriteLine("Rejecting order..."); Console.WriteLine("Approving order...");
approvalResult = ApprovalResult.Rejected; approvalResult = ApprovalResult.Approved;
} }
else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Rejecting order...");
approvalResult = ApprovalResult.Rejected;
}
if (approvalResult != ApprovalResult.Unspecified) if (approvalResult != ApprovalResult.Unspecified)
{ {
// Raise the workflow event to the workflow // Raise the workflow event to the workflow
await daprClient.RaiseWorkflowEventAsync( await daprClient.RaiseWorkflowEventAsync(
instanceId: orderId, instanceId: orderId,
workflowComponent: DaprWorkflowComponent, workflowComponent: DaprWorkflowComponent,
eventName: "ManagerApproval", eventName: "ManagerApproval",
eventData: approvalResult); eventData: approvalResult);
} }
// otherwise, keep waiting // otherwise, keep waiting
}
} }
} }
}
if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed)
{
OrderResult result = state.ReadOutputAs<OrderResult>();
if (result.Processed)
{ {
Console.ForegroundColor = ConsoleColor.Green; OrderResult result = state.ReadOutputAs<OrderResult>();
Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result})."); 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(); 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) static async Task RestockInventory(DaprClient daprClient, List<InventoryItem> inventory)
{ {
Console.WriteLine("*** Restocking inventory..."); Console.WriteLine("*** Restocking inventory...");

View File

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

View File

@ -14,16 +14,20 @@
namespace Dapr.Workflow namespace Dapr.Workflow
{ {
using System; using System;
using Grpc.Net.Client;
using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Worker; using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Net.Http;
using Dapr;
/// <summary> /// <summary>
/// Contains extension methods for using Dapr Workflow with dependency injection. /// Contains extension methods for using Dapr Workflow with dependency injection.
/// </summary> /// </summary>
public static class WorkflowServiceCollectionExtensions public static class WorkflowServiceCollectionExtensions
{ {
/// <summary> /// <summary>
/// Adds Dapr Workflow support to the service collection. /// Adds Dapr Workflow support to the service collection.
/// </summary> /// </summary>
@ -57,7 +61,18 @@ namespace Dapr.Workflow
if (TryGetGrpcAddress(out string address)) 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 else
{ {
@ -85,7 +100,18 @@ namespace Dapr.Workflow
{ {
if (TryGetGrpcAddress(out string address)) 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 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 // 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. // 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. // 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)) { if (!String.IsNullOrEmpty(daprEndpoint)) {
address = daprEndpoint; address = daprEndpoint;
return true; return true;
} }
string? daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT");
if (int.TryParse(daprPortStr, out int daprGrpcPort)) 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 // 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; address = string.Empty;
return false; 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 internal static class DaprDefaults
{ {
private static string httpEndpoint; private static string httpEndpoint = string.Empty;
private static string grpcEndpoint; private static string grpcEndpoint = string.Empty;
private static string daprApiToken; private static string daprApiToken = string.Empty;
private static string appApiToken; private static string appApiToken = string.Empty;
/// <summary> /// <summary>
/// Get the value of environment variable DAPR_API_TOKEN /// 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 // 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 // We don't plan to support the case where the user changes environment variables
// for a running process. // for a running process.
if (daprApiToken == null) if (string.IsNullOrEmpty(daprApiToken))
{ {
// Treat empty the same as null since it's an environment variable // Treat empty the same as null since it's an environment variable
var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN");
daprApiToken = (value == string.Empty) ? null : value; daprApiToken = string.IsNullOrEmpty(value) ? string.Empty : value;
} }
return daprApiToken; return daprApiToken;
@ -47,10 +47,10 @@ namespace Dapr
/// <returns>The value of environment variable APP_API_TOKEN</returns> /// <returns>The value of environment variable APP_API_TOKEN</returns>
public static string GetDefaultAppApiToken() public static string GetDefaultAppApiToken()
{ {
if (appApiToken == null) if (string.IsNullOrEmpty(appApiToken))
{ {
var value = Environment.GetEnvironmentVariable("APP_API_TOKEN"); var value = Environment.GetEnvironmentVariable("APP_API_TOKEN");
appApiToken = (value == string.Empty) ? null : value; appApiToken = string.IsNullOrEmpty(value) ? string.Empty : value;
} }
return appApiToken; return appApiToken;
@ -62,7 +62,7 @@ namespace Dapr
/// <returns>The value of HTTP endpoint based off environment variables</returns> /// <returns>The value of HTTP endpoint based off environment variables</returns>
public static string GetDefaultHttpEndpoint() public static string GetDefaultHttpEndpoint()
{ {
if (httpEndpoint == null) if (string.IsNullOrEmpty(httpEndpoint))
{ {
var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT"); var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT");
if (!string.IsNullOrEmpty(endpoint)) { if (!string.IsNullOrEmpty(endpoint)) {
@ -84,7 +84,7 @@ namespace Dapr
/// <returns>The value of gRPC endpoint based off environment variables</returns> /// <returns>The value of gRPC endpoint based off environment variables</returns>
public static string GetDefaultGrpcEndpoint() public static string GetDefaultGrpcEndpoint()
{ {
if (grpcEndpoint == null) if (string.IsNullOrEmpty(grpcEndpoint))
{ {
var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT");
if (!string.IsNullOrEmpty(endpoint)) { if (!string.IsNullOrEmpty(endpoint)) {