A few days ago, I introduced you StoneAssemblies.MassAuth as a Gatekeeper implementation.
Today, as promised, I bring you a hands-on lab that consists of the following steps:
- Set up the workspace
- Contract first
- Implementing rules
- Implementing services
- Hosting rules
- Build, run and test
So, let’s do this straightforward.
Prerequisites
Step 1: Set up the workspace
To set up the workspace, open a PowerShell console and run the following commands:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
cd $home/source/repos | |
md StoneAssemblies.MassAuth.QuickStart | |
cd StoneAssemblies.MassAuth.QuickStart | |
md deployment/tye | |
Invoke-WebRequest -Uri https://gist.githubusercontent.com/alexfdezsauco/581187a11cb49cec591d2ab7a81e4db5/raw/d1288c330e5eb4192d0d926d47e1e568936989f7/build.cake -OutFile build.cake | |
Invoke-WebRequest -Uri https://gist.githubusercontent.com/alexfdezsauco/761885e61409f5b62c8ce2feb844ce4b/raw/c441f624aef1e115dac4775d79f2eaef0a9fc7ad/tye.yaml -OutFile ./deployment/tye/tye.yaml | |
md src | |
cd src | |
dotnet new sln -n StoneAssemblies.MassAuth.QuickStart | |
dotnet new classlib -n StoneAssemblies.MassAuth.QuickStart.Messages | |
dotnet sln StoneAssemblies.MassAuth.QuickStart.sln add StoneAssemblies.MassAuth.QuickStart.Messages/StoneAssemblies.MassAuth.QuickStart.Messages.csproj | |
dotnet add StoneAssemblies.MassAuth.QuickStart.Messages\StoneAssemblies.MassAuth.QuickStart.Messages.csproj package StoneAssemblies.MassAuth.Messages --prerelease | |
dotnet new classlib -n StoneAssemblies.MassAuth.QuickStart.Rules | |
dotnet sln StoneAssemblies.MassAuth.QuickStart.sln add StoneAssemblies.MassAuth.QuickStart.Rules/StoneAssemblies.MassAuth.QuickStart.Rules.csproj | |
dotnet add StoneAssemblies.MassAuth.QuickStart.Rules/StoneAssemblies.MassAuth.QuickStart.Rules.csproj package StoneAssemblies.MassAuth.Rules --prerelease | |
dotnet add StoneAssemblies.MassAuth.QuickStart.Rules/StoneAssemblies.MassAuth.QuickStart.Rules.csproj reference StoneAssemblies.MassAuth.QuickStart.Messages\StoneAssemblies.MassAuth.QuickStart.Messages.csproj | |
dotnet new webapi -n StoneAssemblies.MassAuth.QuickStart.Services | |
dotnet sln StoneAssemblies.MassAuth.QuickStart.sln add StoneAssemblies.MassAuth.QuickStart.Services/StoneAssemblies.MassAuth.QuickStart.Services.csproj | |
dotnet add StoneAssemblies.MassAuth.QuickStart.Services/StoneAssemblies.MassAuth.QuickStart.Services.csproj package StoneAssemblies.MassAuth --prerelease | |
dotnet add StoneAssemblies.MassAuth.QuickStart.Services/StoneAssemblies.MassAuth.QuickStart.Services.csproj package MassTransit.RabbitMQ --version 7.2.2 | |
dotnet add StoneAssemblies.MassAuth.QuickStart.Services/StoneAssemblies.MassAuth.QuickStart.Services.csproj package Serilog.Sinks.Console --version 4.0.0 | |
dotnet add StoneAssemblies.MassAuth.QuickStart.Services/StoneAssemblies.MassAuth.QuickStart.Services.csproj reference StoneAssemblies.MassAuth.QuickStart.Messages\StoneAssemblies.MassAuth.QuickStart.Messages.csproj | |
dotnet new web -n StoneAssemblies.MassAuth.QuickStart.AuthServer | |
dotnet sln StoneAssemblies.MassAuth.QuickStart.sln add StoneAssemblies.MassAuth.QuickStart.AuthServer/StoneAssemblies.MassAuth.QuickStart.AuthServer.csproj | |
dotnet add StoneAssemblies.MassAuth.QuickStart.AuthServer/StoneAssemblies.MassAuth.QuickStart.AuthServer.csproj package StoneAssemblies.Extensibility --prerelease | |
dotnet add StoneAssemblies.MassAuth.QuickStart.AuthServer/StoneAssemblies.MassAuth.QuickStart.AuthServer.csproj package StoneAssemblies.MassAuth --prerelease | |
dotnet add StoneAssemblies.MassAuth.QuickStart.AuthServer/StoneAssemblies.MassAuth.QuickStart.AuthServer.csproj package StoneAssemblies.MassAuth.Hosting --prerelease | |
dotnet add StoneAssemblies.MassAuth.QuickStart.AuthServer/StoneAssemblies.MassAuth.QuickStart.AuthServer.csproj package MassTransit.RabbitMQ --version 7.2.2 | |
dotnet add StoneAssemblies.MassAuth.QuickStart.AuthServer/StoneAssemblies.MassAuth.QuickStart.AuthServer.csproj package Serilog.Sinks.Console --version 4.0.0 |
After executing these commands, StoneAssemblies.MassAuth.QuickStart.sln Visual Studio solution file is created, which includes the following projects:
- StoneAssemblies.MassAuth.QuickStart.Messages: Class library for messages specification.
- StoneAssemblies.MassAuth.QuickStart.Rules: Class library to implement rules for messages.
- StoneAssemblies.MassAuth.QuickStart.Services: Web API to host the services that require to be authorized by rules.
- StoneAssemblies.MassAuth.QuickStart.AuthServer: Authorization server to host the rules for messages.
The commands also add the required NuGet packages and project references.
If you review the content of the StoneAssemblies.MassAuth.QuickStart.AuthServer.csproj project file, you should notice a package reference to StoneAssemblies.Extensibility. This is required because all rules will be provisioned as plugins for the authorization server.
The extensibility system is NuGet based, so we need to set up the build to provision the rules and messages as NuGet packages. For that is the purpose, this workspace configuration includes two more files. The build.cake, a cake based build script to ensure the required package output,
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var target = Argument("target", "NuGetPack"); | |
var buildConfiguration = Argument("Configuration", "Release"); | |
var NuGetVersionV2 = "1.0.0"; | |
var SolutionFileName = "src/StoneAssemblies.MassAuth.QuickStart.sln"; | |
var ComponentProjects = new string[] | |
{ | |
"./src/StoneAssemblies.MassAuth.QuickStart.Messages/StoneAssemblies.MassAuth.QuickStart.Messages.csproj", | |
"./src/StoneAssemblies.MassAuth.QuickStart.Rules/StoneAssemblies.MassAuth.QuickStart.Rules.csproj" | |
}; | |
Task("Restore") | |
.Does(() => | |
{ | |
DotNetCoreRestore(SolutionFileName); | |
}); | |
Task("Build") | |
.IsDependentOn("Restore") | |
.Does(() => | |
{ | |
DotNetCoreBuild( | |
SolutionFileName, | |
new DotNetCoreBuildSettings() | |
{ | |
Configuration = buildConfiguration, | |
ArgumentCustomization = args => args | |
.Append($"/p:Version={NuGetVersionV2}") | |
.Append($"/p:PackageVersion={NuGetVersionV2}") | |
}); | |
}); | |
Task("NuGetPack") | |
.IsDependentOn("Build") | |
.Does(() => | |
{ | |
string packageOutputDirectory = $"./output/nuget"; | |
EnsureDirectoryExists(packageOutputDirectory); | |
CleanDirectory(packageOutputDirectory); | |
for (int i = 0; i < ComponentProjects.Length; i++) | |
{ | |
var componentProject = ComponentProjects[i]; | |
var settings = new DotNetCorePackSettings | |
{ | |
Configuration = buildConfiguration, | |
OutputDirectory = packageOutputDirectory, | |
IncludeSymbols = true, | |
ArgumentCustomization = args => args | |
.Append($"/p:PackageVersion={NuGetVersionV2}") | |
.Append($"/p:Version={NuGetVersionV2}") | |
}; | |
DotNetCorePack(componentProject, settings); | |
} | |
EnsureDirectoryExists("./output/nuget-symbols"); | |
CleanDirectory("./output/nuget-symbols"); | |
MoveFiles($"{packageOutputDirectory}/*.symbols.nupkg", "./output/nuget-symbols"); | |
var symbolFiles = GetFiles("./output/nuget-symbols/*.symbols.nupkg"); | |
foreach(var symbolFile in symbolFiles) | |
{ | |
var newFileName = symbolFile.ToString().Replace(".symbols", ""); | |
MoveFile(symbolFile, newFileName); | |
} | |
}); | |
RunTarget(target); |
and the tye.yaml that will help us to run and debug the solution.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: stoneassemblies-massauth-quickstart | |
network: stoneassemblies-massauth-quickstart-network | |
services: | |
- name: stoneassemblies-massauth-quickstart-services | |
project: ../../src/StoneAssemblies.MassAuth.QuickStart.Services/StoneAssemblies.MassAuth.QuickStart.Services.csproj | |
buildProperties: | |
- name: Configuration | |
value: Debug | |
env: | |
- name: ASPNETCORE_ENVIRONMENT | |
value: Development | |
- name: RabbitMQ:Address | |
value: rabbitmq://localhost:6002 | |
- name: RabbitMQ:Username | |
value: queuedemo | |
- name: RabbitMQ:Password | |
value: queuedemo | |
bindings: | |
- port: 6001 | |
- name: stoneassemblies-massauth-quickstart-authserver | |
project: ../../src/StoneAssemblies.MassAuth.QuickStart.AuthServer/StoneAssemblies.MassAuth.QuickStart.AuthServer.csproj | |
replicas: 1 | |
buildProperties: | |
- name: Configuration | |
value: Debug | |
env: | |
- name: ASPNETCORE_ENVIRONMENT | |
value: Development | |
- name: RabbitMQ:Address | |
value: rabbitmq://localhost:6002 | |
- name: RabbitMQ:Username | |
value: queuedemo | |
- name: RabbitMQ:Password | |
value: queuedemo | |
- name: Extensions:Sources:0 | |
value: ../../output/nuget-symbols/ | |
- name: Extensions:Sources:1 | |
value: https://api.nuget.org/v3/index.json | |
- name: Extensions:Packages:0 | |
value: StoneAssemblies.MassAuth.QuickStart.Rules | |
- name: stoneassemblies-massauth-quickstart-rabbitmq | |
image: rabbitmq:3.8.3-management | |
bindings: | |
- name: rabbitmq | |
port: 6002 | |
containerPort: 5672 | |
protocol: tcp | |
- name: https | |
port: 6003 | |
containerPort: 15672 | |
protocol: http | |
env: | |
- name: RABBITMQ_DEFAULT_USER | |
value: queuedemo | |
- name: RABBITMQ_DEFAULT_PASS | |
value: queuedemo |
Step 2: Contract first
Let's add a bit of complexity to the generated problem, related to the weather forecast. For instance, let's say we will allow requesting forecasts from a certain date, as some forecasts may not be available due to the complexity of the calculations.
For that purpose, we will add the following class to the message project, to request the weather forecast with the start date as an argument.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace StoneAssemblies.MassAuth.QuickStart.Messages | |
{ | |
using System; | |
using StoneAssemblies.MassAuth.Messages; | |
public class WeatherForecastRequestMessage : MessageBase | |
{ | |
public DateTime StartDate { get; set; } | |
} | |
} |
Step 3: Implementing rules
Now we are ready to implement some rules for such a message. Continuing with our scenario, let's say the forecast data is only available from today and up to 10 days. This operation could be more complex through a query to an external database, but for simplicity, it will be implemented as follows.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace StoneAssemblies.MassAuth.QuickStart.Rules | |
{ | |
using System; | |
using System.Threading.Tasks; | |
using StoneAssemblies.MassAuth.Messages; | |
using StoneAssemblies.MassAuth.QuickStart.Messages; | |
using StoneAssemblies.MassAuth.Rules.Interfaces; | |
public class DataAvailabilityDateRule : IRule<AuthorizationRequestMessage<WeatherForecastRequestMessage>> | |
{ | |
public bool IsEnabled { get; } = true; | |
public string Name { get; } = "Data availability date"; | |
public int Priority { get; } | |
public async Task<bool> EvaluateAsync(AuthorizationRequestMessage<WeatherForecastRequestMessage> message) | |
{ | |
var days = message.Payload.StartDateTime.Subtract(DateTime.Now).TotalDays; | |
return days >= 0 && days < 10; | |
} | |
} | |
} |
Step 4: Implementing services
It's time to complete the WeatherForecastController implementation in the service project. It should look like this.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace StoneAssemblies.MassAuth.QuickStart.Services.Controllers | |
{ | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.Extensions.Logging; | |
using StoneAssemblies.MassAuth.QuickStart.Messages; | |
using StoneAssemblies.MassAuth.Services.Attributes; | |
[ApiController] | |
[Route("[controller]")] | |
public class WeatherForecastController : ControllerBase | |
{ | |
private static readonly string[] Summaries = | |
{ | |
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" | |
}; | |
private readonly ILogger<WeatherForecastController> _logger; | |
public WeatherForecastController(ILogger<WeatherForecastController> logger) | |
{ | |
_logger = logger; | |
} | |
[HttpGet] | |
[AuthorizeByRule] | |
public IEnumerable<WeatherForecast> Get([FromQuery] WeatherForecastRequestMessage weatherForecastRequestMessage) | |
{ | |
var rng = new Random(); | |
return Enumerable.Range(1, 5).Select( | |
index => new WeatherForecast | |
{ | |
Date = weatherForecastRequestMessage.StartDate.AddDays(index), | |
TemperatureC = rng.Next(-20, 55), | |
Summary = Summaries[rng.Next(Summaries.Length)] | |
}).ToArray(); | |
} | |
} | |
} |
Notice the usage of AuthorizeByRule attribute on the Get method, to indicate that the input message WeatherForecastRequestMessage must be processed and validated by the authorization engine before the method execution.
We also have to update the Startup class implementation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace StoneAssemblies.MassAuth.QuickStart.Services | |
{ | |
using MassTransit; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.OpenApi.Models; | |
using StoneAssemblies.MassAuth.QuickStart.Messages; | |
using StoneAssemblies.MassAuth.Services; | |
using StoneAssemblies.MassAuth.Services.Extensions; | |
public class Startup | |
{ | |
public Startup(IConfiguration configuration) | |
{ | |
this.Configuration = configuration; | |
} | |
public IConfiguration Configuration { get; } | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
app.UseSwagger(); | |
app.UseSwaggerUI( | |
c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "StoneAssemblies.MassAuth.QuickStart.Services v1")); | |
} | |
app.UseHttpsRedirection(); | |
app.UseRouting(); | |
app.UseAuthorization(); | |
app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); | |
} | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddControllers(); | |
services.AddSwaggerGen( | |
c => | |
{ | |
c.SwaggerDoc( | |
"v1", | |
new OpenApiInfo | |
{ | |
Title = "StoneAssemblies.MassAuth.QuickStart.Services", | |
Version = "v1" | |
}); | |
}); | |
services.AddMassAuth(); | |
var username = this.Configuration.GetSection("RabbitMQ")?["Username"] ?? "queuedemo"; | |
var password = this.Configuration.GetSection("RabbitMQ")?["Password"] ?? "queuedemo"; | |
var messageQueueAddress = this.Configuration.GetSection("RabbitMQ")?["Address"] ?? "rabbitmq://localhost"; | |
services.AddMassTransit( | |
sc => | |
{ | |
sc.AddBus( | |
context => Bus.Factory.CreateUsingRabbitMq( | |
cfg => | |
{ | |
cfg.Host( | |
messageQueueAddress, | |
configurator => | |
{ | |
configurator.Username(username); | |
configurator.Password(password); | |
}); | |
})); | |
sc.AddDefaultAuthorizationRequestClient<WeatherForecastRequestMessage>(); | |
}); | |
services.AddHostedService<BusHostedService>(); | |
} | |
} | |
} |
Basically, the AddMassAuth service collection extension method is called to register the library services and also ensure communication through the message broker. Remember, StoneAssemblies.MassAuth is built on top of MassTransit. Finally, to read the configuration via environment variables we must update the Program class to this.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace StoneAssemblies.MassAuth.QuickStart.Services | |
{ | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.Hosting; | |
using Serilog; | |
public class Program | |
{ | |
public static IHostBuilder CreateHostBuilder(string[] args) | |
{ | |
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); | |
return Host.CreateDefaultBuilder(args) | |
.ConfigureAppConfiguration(builder => { builder.AddEnvironmentVariables(); }) | |
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); | |
} | |
public static void Main(string[] args) | |
{ | |
CreateHostBuilder(args).Build().Run(); | |
} | |
} | |
} |
Step 5: Hosting rules
To host rules, we provide a production-ready of StoneAssemblies.MassAuth.Server as docker image available in DockerHub. But for debugging or even for customization purpose could be useful build your own rule host server. So, in the server project, we also have to update the Startup class implementation, to initialize the extensibility system and load rules.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace StoneAssemblies.MassAuth.QuickStart.AuthServer | |
{ | |
using GreenPipes; | |
using MassTransit; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Serilog; | |
using StoneAssemblies.Extensibility.Extensions; | |
using StoneAssemblies.MassAuth.Hosting.Extensions; | |
using StoneAssemblies.MassAuth.Services; | |
public class Startup | |
{ | |
public Startup(IConfiguration configuration) | |
{ | |
this.Configuration = configuration; | |
} | |
public IConfiguration Configuration { get; } | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseRouting(); | |
app.UseEndpoints( | |
endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); | |
} | |
public void ConfigureServices(IServiceCollection serviceCollection) | |
{ | |
serviceCollection.AddExtensions(this.Configuration); | |
serviceCollection.AddRules(); | |
var username = this.Configuration.GetSection("RabbitMQ")?["Username"] ?? "queuedemo"; | |
var password = this.Configuration.GetSection("RabbitMQ")?["Password"] ?? "queuedemo"; | |
var messageQueueAddress = this.Configuration.GetSection("RabbitMQ")?["Address"] ?? "rabbitmq://localhost"; | |
serviceCollection.AddMassTransit( | |
sc => | |
{ | |
sc.AddAuthorizationRequestConsumers(); | |
Log.Information("Connecting to message queue server with address '{ServiceAddress}'", messageQueueAddress); | |
sc.AddBus( | |
context => Bus.Factory.CreateUsingRabbitMq( | |
cfg => | |
{ | |
cfg.Host( | |
messageQueueAddress, | |
configurator => | |
{ | |
configurator.Username(username); | |
configurator.Password(password); | |
}); | |
sc.ConfigureAuthorizationRequestConsumers( | |
(messagesType, consumerType) => | |
{ | |
cfg.DefaultReceiveEndpoint( | |
messagesType, | |
e => | |
{ | |
e.PrefetchCount = 16; | |
e.UseMessageRetry(x => x.Interval(2, 100)); | |
e.ConfigureConsumer(context, consumerType); | |
}); | |
}); | |
})); | |
}); | |
serviceCollection.AddHostedService<BusHostedService>(); | |
} | |
} | |
} |
Again, to read the configuration via environment variables the Program file must be updated like this.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace StoneAssemblies.MassAuth.QuickStart.AuthServer | |
{ | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.Hosting; | |
using Serilog; | |
public class Program | |
{ | |
public static IHostBuilder CreateHostBuilder(string[] args) | |
{ | |
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); | |
return Host.CreateDefaultBuilder(args) | |
.ConfigureAppConfiguration(builder => { builder.AddEnvironmentVariables(); }) | |
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); | |
} | |
public static void Main(string[] args) | |
{ | |
CreateHostBuilder(args).Build().Run(); | |
} | |
} | |
} |
Step 6: Build, run and test
Let's see if this works. So, cross your fingers first ;)
To build and run the project, open a PowerShell terminal in the working directory and run the following commands.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
dotnet cake | |
cd deployment/tye | |
tye run |
Open your browser and navigate to http://localhost:8000 to display the Tye user interface.
Let's do some weather forecast requests. For instance with a valid request
the output shows an unauthorized response.
So, as expected, it works ;)
Closing
In case it doesn't work for you, you can always try to review the final and complete source code of this hands-on lab is in the StoneAssemblies.MassAuth.QuickStart repository is available on GitHub.
Remember StoneAssemblies.MassAuth is a work in progress, we are continuously releasing new versions, so your feedback is welcome. Also, remember that it is an open-source project, so you can contribute too.
Enjoy «authorizing» with pleasure and endless possibilities.