Introduction
The extension on Domain-Driven Design (DDD) is Domain-Driven Security which is actually “DDD with conventions”. Personally I would think that conventions, validation, … is already a part of domain-modeling and design (and it is), but we could go even further. This post explores ways to secure your domain model even more by reusable validations, ways to define trust boundaries, and increase control on the access of your domain model.
The Reality of the Naive
Did you know that input validation is one of the major vulnerabilities when it comes to security on application-level? How is it that we somehow ‘trust’ inputs from external sources?
One of the major topics of security in application development is defining trust-boundaries: from what level do we assume that we are dealing with input that we can trust? Something that is not null
? A string
that is not empty? … Probably. But is that correct?
Sometimes you see projects where there exists some validation with white-listing input via regular expressions; which is probably the best way to have a secure validation system. But after the validation, the same “untrusted” data type is used around the system. How can you be sure that that data is still valid? To be correct, you should duplicate your entire validation on every single place that untrusted data type is used. Or… you could create a dedicated type for it to centralize the validation. This is the one and only correct approach to address validation!
The FPrimitive library contains a validation system to address both validation and domain modeling because they should indeed be addressed at the same time.
Following example shows how validation and modeling can be combined (just for demonstration purposes).
using FPrimitive; | |
public class ProductName | |
{ | |
private readonly string _value; | |
/// Prevents a default instance of the type from being created. | |
private ProductName(string value) | |
{ | |
_value = value; | |
} | |
/// Creates a product name instance based on untrusted data. | |
public static ValidationResult<ProductName> Create(string untrusted) | |
{ | |
return Spec.Of<string>() | |
.NotNull("Product name should not be 'null'") | |
.NotEmpty("Product name should not be empty") | |
.NotWhiteSpace("Product name should not contain only white-space characters") | |
.Regex("^[a-zA-Z ]+$", "Product name should only contain characters from a-z") | |
.CreateModel(untrusted, validated => new ProductName(validated)); | |
} | |
} |
open FPrimitive | |
/// Composible specifications for your domain types: | |
type NonEmptyString = | |
private NonEmptyString of string with | |
static member create x = | |
Spec.def<string> | |
|> Spec.notNull "should not be null" | |
|> Spec.notEmpty "should not be empty" | |
|> Spec.createModel NonEmptyString x | |
/// ...also available as computation expression. | |
type NonEmptyList<'a> = | |
private NonEmptyList of 'a list with | |
static member create xs = | |
specModel NonEmptyList xs { | |
nonEmpty "list should not be empty" | |
lengthBetween 1 10 "list length should be between 1-10" } |
Control is an Illusion
After you establish your domain types, your specifications and your trust boundaries, you can think about how your application should access certain parts. Is one path more critical than another? Is it really necessary to keep some private information in memory the whole time the application is running? Is it correct that some function can be called more than once?
Several of these questions can be handled within the infrastructure of your application. If you control from the outside when your app is available for example. But in my opinion, that should not be an excuse to not think about Access-Control on application level. Maybe there is another way to access your application to bypass the infrastructure. What happens then?
The FPrimitive contains, besides of specifications, also a way to make your part of the application more controlled. Here are some examples:
// Specification before the actual check of the base-uri. | |
Spec<Uri> spec = | |
Spec.Of<Uri>() | |
.Add(uri => uri.Scheme == Uri.UriSchemeHttps, "should be 'https' scheme"); | |
// Demonstration purposes, generic type could be anything. | |
IObservable<T> obs = null; | |
// Access controlled function with validation, revocation, limited amount of evaluations and time-based availability. | |
Access<Uri, Uri> access = | |
Access.OnlyUriFromBase(new Uri("https://localhost:9090/")) | |
.Satisfy(spec) | |
.Once() | |
.DuringHours(9, 17) | |
.RevokedWhen(obs); | |
// Somewhere else... | |
AccessResult<Uri> result = access.Eval(new Uri("http://localhost:9090/path")); | |
if (result.TryGetValue(out Uri output)) | |
{ | |
// use the now correct 'output' | |
} |
// Specification that the input parameter of the access-controlled function should satisfy | |
let spec = | |
Spec.def<string> | |
|> Spec.notEmpty "should not be empty string" | |
// The function (fun x -> Some x) is now 'access-controlled' with a specifiation for non-empty strings, | |
// can be revoked anytime the 'Access.revoke acc' is called, and can only be accessed three times. | |
let acc = | |
Access.func (fun x -> Some x) | |
|> Access.satisfy spec | |
|> Access.revokable | |
|> Access.times 3 | |
// Revoke the function | |
Access.revoke acc | |
// Only files from within the '/bin' directory and file extension '.txt' are allowed to pass | |
let acc = access { | |
onlyFilesFrom (DirectoryInfo "/bin") | |
fileExtension ".txt" } | |
// Evalute 'access-controlled' function with file '/bin/image.jpg' | |
let (result : Result<FileInfo, string list>) = Access.eval (FileInfo "/bin/image.jpg") acc | |
Trust is not an Option
As a bonus, I want to show how we can manipulate the default untrusted data (int
, string
, byte
, …) and make it explicit that this is indeed untrusted data.
The FPrimitive package contains a way to do this. The idea is to teach people how small the circle of trust really is.
int unknown = 5; | |
var untrusted = Untrust<int>(unknown); | |
if (untrusted.TryGetValue(x => x > 3, out int trusted)) | |
{ | |
// Set 'trusted' now into dedicated 'Int' type. | |
} |
open FPrimitive | |
let unknown = 10 | |
let untrusted = Untrust unknown | |
type NonZeroInt = | |
private NonZeroInt of int with | |
static member create x = | |
Spec.def<int> | |
|> Spec.notEqual 0 "should not be zero" | |
|> Spec.createModel NonZeroInt x | |
let (trusted : Result<NonZeroInt, string list>) = | |
Untrust.getWithResult NonZeroInt.create untrusted |
Conclusion
Having these building blocks in your next project will help you creating a more correct Domain model and by doing so, a more secure application.
Thank you for reading!
Subscribe to our RSS feed