Aggregate functions
chm.Concat
This function concatenates varchar/nvarchar columns in aggregation queries.
Example:
Suppose we have the following tables.
dbo.Books
Column | Type |
---|---|
id |
int
|
title |
varchar(200)
|
year |
int
|
dbo.Tags
Column | Type |
---|---|
id |
int
|
name |
varchar(100)
|
dbo.BookTags
Column | Type |
---|---|
bookId |
int
|
tagId |
int
|
We can write a query like below to get list of books and their tags.
select
b.[title],
chm.Concat(t.[name], ',') as [tags]
from dbo.Books b
inner join dbo.BookTags bt on b.[id] = bt.[bookId]
inner join dbo.Tags t on bt.[tagId] = t.[id]
group by b.[title]
chm.BitwiseAnd / chm.BitwiseOr
These functions apply bitwise and/or merge on integer columns in aggregation queries.
Practical Example, ACL system
In this section we implement a simple ACL system to show a practical usecase for BitwiseAnd
nad BitwiseOr
aggregate functions. List of tables we use are as follows:
dbo.Users
Column | Type |
---|---|
id |
int
|
username |
varchar(50)
|
password |
varchar(50)
|
dbo.Roles
Column | Type |
---|---|
id |
int
|
name |
varchar(50)
|
dbo.Entities
Column | Type |
---|---|
id |
int
|
name |
varchar(100)
|
dbo.RolePermissions
Column | Type |
---|---|
roleId |
int
|
entityId |
int
|
value |
tinyint
|
dbo.UserRoles
Column | Type |
---|---|
userId |
int
|
roleId |
int
|
In this ACL system permissions are defined over entities and they are granted to roles, not users. Each role can have a number of permissions over an entity.
Permissions of a role is defined as a tinyint
value. It holds a combination of permissions. Each bit in a tinyint
grant value means a specific permission. They are listed in the following table:
Bit Number | Meaning |
---|---|
0 | View |
1 | Add |
2 | Edit |
3 | Delete |
4 | Reserved |
5 | Reserved |
6 | Reserved |
7 | Reserved |
As it is seen, our ACL system supports 4 permissions. The rest 4 bits are reserved for later usage. Having this system, for a role like Operator
to have View/Add/Edit
permissions over 'Book' entity, a 7 value will be calculated.
R R R R D E A V
0 0 0 0 0 1 1 1 = 7
And permission value for an Admin
role with full control over 'Book' entity would be 15.
R R R R D E A V
0 0 0 0 1 1 1 1 = 15
Now, since users can have multiple roles, a user can have multiple permissions over an entity. We need to perform a bitwise merge operation in order to retrieve final permissions of a user.
In fact, we need to have a polciy to decide what we should do when a user has multiple same permissions over an entity (e.g. he has an Add
permission over Books
using his Admin
role, but he does not have such a permission over Books
using his Member
role).
We can use a bitwise merge over user permissions. This could be performed both as a bitwise-or or bitwise-and. It depends on our policy which one to choose. A bitwise-or is more permissive while a bitwise-and is more restrictive.
Now, we can write a helper UDF like below to get permissions of a given user over an entity:
create or alter dbo.GetPermission
(
@username varchar(50),
@entity varchar(100)
)
returns tinyint
as
begin
declare @result tinyint
select
@result = chm.BitwiseOr(rp.[vale])
from dbo.Users u
inner join dbo.UserRoles ur on u.[id] = ur.[userId]
inner join dbo.Roles r on ur.[roleId] = r.[id]
inner join dbo.RolePermissions rp on r.[roleId] = rp.[roleId]
inner join dbo.Entities e on rp.[entityId] = e.[id]
where u.[username] = @username and e.[name] = @entity
set @result = isnull(@result, 0)
return @result
end
Here, we used bitwise-or policy. In order to switch to bitwise-and, we can simply replace chm.BitwiseOr()
with chm.BitwiseAnd()
.
This simple and easy-to-use ACL system could be used in any type of application with any type of technology. After retrieving permissions of a user over an entity we can decide to show or hide buttons like 'Add', 'Edit', 'Delete' in say, administration panel of the application.
Here is a helper C# class that provides boolean getter properties for a loaded permission of a user:
public class UserPermission
{
public readonly byte Value { get; private set; }
public UserPermission(byte value)
{
Value = value;
}
public bool Check(byte index)
{
var bitValue = Math.Power(2, index);
return Value & bitValue == bitValue;
}
public bool View
{
get { return Check(0); }
}
public bool Add
{
get { return Check(1); }
}
public bool Edit
{
get { return Check(2); }
}
public bool Delete
{
get { return Check(3); }
}
}
Having this class, using it will be easy. The following is a Web API that returns permission of current user for a requested entity.
[ApiController]
[Route("[controller]/[entity]")]
public class PermissionController: ControllerBase
{
readonly IConfiguration __configuration;
public PermissionController(IConfiguration configuration)
{
_configuration = configuration;
}
public UserPermission Get(string entity)
{
var constr = _configuration.GetConnectionString("DefaultConnection");
using (var con = new SqlConnection(constr))
{
using (var cmd = new SqlCommand("select dbo.GetPermission(@username, @entity)", con)))
{
cmd.Parameters.AddWithValue("@username", User.Identity.Name);
cmd.Parameters.AddWithValue("@entity", entity);
var permission = Convert.ToByte(cmd.ExecuteScaler());
return new UserPermission(permission);
}
}
}
}
If we use, Util.Db
in Chameleon.Core
, we can make Get()
action simpler:
...
public UserPermission Get(string entity)
{
Util.Db.ConnectionString = _configuration.GetConnectionString("DefaultConnection");
var permission = Util.Db.ExecuteSingleSql("select dbo.GetPermission(@username, @entity)",
reader => new UserPermission(Convert.ToByte(reader[0])),
new { username = User.Identity.Name, entity = entity });
return permission;
}
For the final piece to complete the puzzle, here is a sample BookList
React component that displays list of books and conditionally shows Add/Edit/Delete buttons based on current user's permissions fetched from the server -using the API provided above.
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Button } from 'reactstrap';
const BookList = () => {
const [books, setBooks] = useState([]);
const [permission, setPermission] = useState({});
const fetchBooks = async () => {
const res = await fetch('/api/books');
const books = await res.json();
setBooks(books);
}
useEffect(() => {
(async () => {
const res = await fetch('/api/permission/books');
const permission = await res.json();
setPermission(permission);
await fetchBooks();
})();
}, []);
const handleDelete = async (bookId) => {
if (confirm('Are you sure?')) {
const res = await fetch(`/api/books/${bookId}`, {
method: 'DELETE'
});
const result = await res.json();
if (result) {
await fetchBooks();
}
}
}
return <>
{permission.add ? <Link to="/books/add">
<Button variant="primary" size="md">
Add
</Button>
</Link>: null}
<Table className="table">
<thead>
<tr>
<th>#</th>
{permission.edit ? <th></th>: null}
{permission.delete ? <th></th>: null}
<th>Title</th>
<th>Year</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{books.map((book, index) => <tr>
<th scope="row">{index}</th>
<td>{book.title}</td>
{permission.edit ? <td>
<Link to={`/books/edit/${book.id}`}>
<Button variant="success" size="md">
Edit
</Button>
</Link>
</td>: null}
{permission.delete ? <td>
<Button variant="danger" size="md" onClick={() => handleDelete(book.id)}>
Delete
</Button>
</td>: null}
<td>{book.title}</td>
<td>{book.year}</td>
<td>{book.price}</td>
</tr>)}
</tbody>
</Table>
</>;
}
export default BookList;