Showing posts with label CakeBuild. Show all posts
Showing posts with label CakeBuild. Show all posts

Sunday, August 22, 2021

StoneAssemblies.MassAuth Hands-on Lab

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:

  1. Set up the workspace
  2. Contract first
  3. Implementing rules
  4. Implementing services
  5. Hosting rules
  6. Build, run and test
So, let’s do this straightforward. 

Prerequisites

  • Visual Studio 2019 (16.9.3)
  • Docker (2.3.0.4)
  • Cake (1.1.0)
  • Tye (0.9.0-alpha.21380.1)

Step 1: Set up the workspace

To set up the workspace, open a PowerShell console and run the following commands:

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
view raw setup.ps1 hosted with ❤ by GitHub

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.ServicesWeb API to host the services that require to be authorized by rules.
  • StoneAssemblies.MassAuth.QuickStart.AuthServerAuthorization 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,  

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);
view raw build.cake hosted with ❤ by GitHub

and the tye.yaml that will help us to run and debug the solution.
 
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
view raw tye.yaml hosted with ❤ by GitHub


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.
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.


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. 

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.

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. 

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. 


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. 


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.

dotnet cake
cd deployment/tye
tye run

Open your browser and navigate to http://localhost:8000 to display the Tye user interface. 


You can see the logs of the rules host server to notice how extensibility works.


Let's do some weather forecast requests. For instance with a valid request

Invoke-WebRequest http://localhost:6001/WeatherForecast?StartDate=$([System.DateTime]::Now.AddDays(1).Date)
the output looks like this


but with an out of range request

Invoke-WebRequest http://localhost:6001/WeatherForecast?StartDate=$([System.DateTime]::Now.AddDays(11).Date)

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.

X-ray StoneAssemblies.MassAuth with NDepend

Introduction A long time ago, I wrote this post  Why should you start using NDepend?  which I consider as the best post I have ever...