What is Suplex?
Suplex.Security is an application security and RBAC abstraction Layer, which implements a hierarchical DACL model and common role/task RBAC model. Suplex is suitable for use in any application/API.
Philosophy
The intent of Suplex is:
- To support a flexible, adaptable, pluggable RBAC that's abstracted from core application design,
- To avoid tightly-coupling the security Role implementation to application features or modules, and thus:
- Avoid "role bleed," where existing Role-implementation to app-feature encompasses too great a scope, and over time requires code maintenance to narrow the scope, where otherwise there is insufficient granularity in Roles manifested at the point of consumption.
Learn More
Read about using Suplex and best practices, check out the code and find downloads on GitHub, or if tl;dr, jump in to the QuickStart below. A deep-dive on the SampleApp from the QuickStart can be found here.
QuickStart
Wanna get started without a lot of pain? Me too. Given the philosophical statement above, you won't need much code to use Suplex in your application. This quick start guide assumes a working knowledge of Visual Studio, C#, and NuGet.
Get the Admin UI
Download the latest release of Suplex.UI.Wpf from GitHub. To get started, extract the zip and run the exe.
1. Create some Security Principals and Secure Objects
-
From the toolbar, choose Security Principals, then, from the left panel, choose New > New User.
- Complete the properties in the right panel, then click Save. Create a few Users.
-
Choose New > New Group, and create a few Groups in a similar fashion to Users.
- For each new Group, choose the Local (Suplex) option and add some Users in the bottom Members list.
-
Now choose Secure Objects from the toolbar and then New > New Root from the left panel.
- Complete the UniqueName field, where the Secure Object's UniqueName is the key field you'll use at runtime to identify the security entries in the Suplex store.
- In the Permissions grid, choose New Permission > UIRight. Select a Group and select the desired permission settings.
-
Once you're finished creating Security Principals and Secure Objects, save your work to a file - click the Save Suplex File Store icon from the toolbar.
Click here to expand this section for animated samples of editing a Suplex FileStore.
2. Use the Suplex FileStore in an App
This example uses a simple WinForms app to demonstrate Suplex integration. The highlights are:
-
NuGet references to Suplex.Security.Core and Suplex.Security.FileSystemDal.
-
A FileWatcher to detect updates to the FileStore and dynamically reload security information.
-
A combobox to simulate switching security context.
-
An "Employees DataAccessLayer" (fake DAL to simulate data security paradigms)
-
Full source here: Suplex.Sample
Most of the code in the sample app is setup-oriented, not functionally relevant to Suplex itself. Below are the key functions to instantiating the FileSystemDal and selecting security at runtime. The critical code elements are:
//Load the Suplex FileStore from disk
_suplexDal = FileSystemDal.LoadFromYamlFile( filestorePath );
//Eval the security for an object at runtime.
//Note: The return value may be null if the object is not found, the user is disabled, or for other reasons.
// Be sure to null-check usage.
SecureObject secureObject =
(SecureObject)_suplexDal.EvalSecureObjectSecurity( "frmEditor", ((User)cmbUsers.SelectedItem).Name );
//Assess 'AccessAllowed' (bool) for the object or a descendant (child) object
secureObject?.Security.Results.GetByTypeRight( UIRight.Visible ).AccessAllowed;
secureObject?.FindChild<SecureObject>( "lblEmployeeId" ).Security.Results.GetByTypeRight( UIRight.Visible ).AccessAllowed;
In context, here's the same code as applied within the relevant methods:
public partial class MainDlg : Form
{
FileSystemDal _suplexDal = new FileSystemDal();
/// <summary>
/// Loads the specified file into a Suplex FileSystemDal and refreshes the dialog
/// </summary>
/// <param name="filestorePath"></param>
void RefreshSuplex(string filestorePath)
{
_suplexDal = FileSystemDal.LoadFromYamlFile( filestorePath );
this.UiThreadHelper( () => cmbUsers.DataSource =
new BindingSource( _suplexDal.Store.Users.OrderBy( u => u.Name ).ToList(), null ).DataSource );
}
#region Apply Security
/// <summary>
/// Simulates switching the current security context
/// </summary>
private void cmbUsers_SelectedIndexChanged(object sender, EventArgs e)
{
string currentUser = ((User)cmbUsers.SelectedItem).Name;
//set the "current user" on the Employees DAL
_employeeDal.CurrentUser = currentUser;
//refresh the Employees list based on "currentUser"
RefreshEmployeesList();
//Evaluate the security information, starting from the top-most control
SecureObject secureObject = (SecureObject)_suplexDal.EvalSecureObjectSecurity( "frmEditor", currentUser );
//apply security to frmEditor/children
ApplyRecursive( secureObject );
}
/// <summary>
/// Recursively examines frmEditor and its children for applying security; see UIExtensions
/// </summary>
/// <param name="secureObject">The matching SecureObject to frmEditor</param>
void ApplyRecursive(SecureObject secureObject)
{
frmEditor.ApplySecurity( secureObject );
}
/// <summary>
/// Brute-force permissioning - direct lookup of results with "known" translation of non-UI rights (not preferred)
/// </summary>
/// <param name="secureObject">A reference to the resolved/evaluated security object.</param>
void ApplyDirect(SecureObject secureObject)
{
frmEditor.Visible = secureObject?.Security.Results.GetByTypeRight( UIRight.Visible ).AccessAllowed ?? false;
lblEmployeeId.Visible =
secureObject?.FindChild<SecureObject>( "lblEmployeeId" ).Security.Results.GetByTypeRight( UIRight.Visible ).AccessAllowed ?? false;
btnUpdate.Enabled =
secureObject?.FindChild<SecureObject>( "btnUpdate" ).Security.Results.GetByTypeRight( RecordRight.Update ).AccessAllowed ?? false;
}
#endregion
private void RefreshEmployeesList()
{
try
{
lstEmployees.DisplayMember = "Name";
lstEmployees.Items.Clear();
List<Employee> employees = _employeeDal.GetEmployees()?.OrderBy( emps => emps.Name ).ToList();
if( employees != null )
foreach( Employee employee in employees )
lstEmployees.Items.Add( employee );
lstMessages.Items.Insert( 0, $"Info : Retrieved {employees?.Count ?? 0} Employee records." );
}
catch( Exception ex )
{
lstMessages.Items.Insert( 0, $"Error: {ex.Message}" );
}
}
}
public static class UIExtensions
public static void ApplySecurity(this Control control, ISecureObject secureObject)
{
if( secureObject == null )
{
control.Visible = false;
return;
}
ISecureObject found = secureObject.UniqueName.Equals( control.Name, StringComparison.OrdinalIgnoreCase ) ?
secureObject : secureObject.FindChild<ISecureObject>( control.Name );
if( found != null && found.Security.Results.ContainsRightType( typeof( UIRight ) ) )
{
control.Visible = found.Security.Results.GetByTypeRight( UIRight.Visible ).AccessAllowed;
control.Enabled = found.Security.Results.GetByTypeRight( UIRight.Enabled ).AccessAllowed;
if( control is TextBox textBox )
textBox.ReadOnly = !found.Security.Results.GetByTypeRight( UIRight.Operate ).AccessAllowed;
}
if( control.HasChildren )
foreach( Control child in control.Controls )
child.ApplySecurity( secureObject );
}
}
Here are sample DAL methods:
public class EmployeeDataAccessLayer
{
ISuplexDal _suplexDal = null;
List<Employee> _employees = null;
#region ctor
public EmployeeDataAccessLayer(ISuplexDal suplexDal)
{
//init Suplex DAL instance
_suplexDal = suplexDal;
//create some Employees
_employees = new List<Employee>
{
{ new Employee( 1 ) { Name = "Irma Tahnee" } },
{ new Employee( 2 ) { Name = "Cat Maxwell" } },
{ new Employee( 3 ) { Name = "Krystelle Padma" } },
{ new Employee( 4 ) { Name = "Louis Zola" } },
{ new Employee( 5 ) { Name = "Esmaralda Grahame" } }
};
}
#endregion
#region Security implementation
/// <summary>
/// "Current user context" - normally this would be pulled from the environment as a local or web current user context
/// </summary>
public string CurrentUser { get; set; }
/// <summary>
/// Utility method to validate security access for a given right on Employee records
/// </summary>
/// <param name="recordRight">The right for which to validate access</param>
bool HasAccess(RecordRight recordRight)
{
//Look up security information by SecureObject->UniqueName => "EmployeeRecords" for the CurrentUser
SecureObject employeeSecurity = (SecureObject)_suplexDal.EvalSecureObjectSecurity( "EmployeeRecords", CurrentUser );
//Assess AccessAllowed
return employeeSecurity?.Security.Results.GetByTypeRight( recordRight ).AccessAllowed ?? false;
}
/// <summary>
/// Utility method to validate security access for a given right on Employee records
/// </summary>
/// <param name="recordRight">The right for which to validate access</param>
void HasAccessOrException(RecordRight recordRight)
{
//Look up security information by SecureObject->UniqueName => "EmployeeRecords" for the CurrentUser
SecureObject employeeSecurity = (SecureObject)_suplexDal.EvalSecureObjectSecurity( "EmployeeRecords", CurrentUser );
//Assess AccessAllowed, throw Exception if no rights
if( !employeeSecurity?.Security.Results.GetByTypeRight( recordRight ).AccessAllowed ?? true )
throw new Exception( $"{CurrentUser} does not have rights to {recordRight} Employee records." );
}
#endregion
#region CRUD methods
/// <summary>
/// Get the Employees list
/// </summary>
/// <param name="filter">Optional Name filter</param>
/// <returns>All or matching Employees</returns>
public List<Employee> GetEmployees(string filter = null)
{
//Check for access rights, throws exception if denied
HasAccessOrException( RecordRight.List );
if( !string.IsNullOrWhiteSpace( filter ) )
return _employees.FindAll( e => e.Name.Contains( filter ) );
else
return _employees;
}
/// <summary>
/// Finds Employee by Id and updates the fields
/// </summary>
/// <param name="emp">The Employee record to find (by Id)</param>
/// <returns>The updated Employee record if found, or null</returns>
public Employee UpdateEmployee(Employee emp)
{
//Check for access rights, throws exception if denied
HasAccessOrException( RecordRight.Update );
if( emp == null )
return null;
Employee found = _employees.FirstOrDefault( e => e.Id == emp.Id );
if( found != null )
found.Name = emp.Name;
return found;
}
#endregion
}