mirror of https://github.com/dapr/dotnet-sdk.git
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:
parent
fd7168fdfc
commit
87329f62b1
|
@ -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.
|
||||
|
|
|
@ -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...");
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Reference in New Issue