Skip to content

Commit

Permalink
Merge pull request #14 from romandykyi/implement-permissions-checker
Browse files Browse the repository at this point in the history
Implement Permissions Checker
  • Loading branch information
romandykyi authored Mar 6, 2024
2 parents ed36df3 + be2cce7 commit 606a685
Show file tree
Hide file tree
Showing 10 changed files with 656 additions and 2 deletions.
13 changes: 13 additions & 0 deletions AdvancedTodoList.Core/Dtos/PermissionsAggregate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using AdvancedTodoList.Core.Models.TodoLists.Members;

namespace AdvancedTodoList.Core.Dtos;

/// <summary>
/// Represents an aggregate of a to-do list member with a role.
/// </summary>
public record PermissionsAggregate(RoleEssentials? Role);

/// <summary>
/// Represents role's permissions and priority.
/// </summary>
public record RoleEssentials(int Priority, RolePermissions Permissions);
12 changes: 12 additions & 0 deletions AdvancedTodoList.Core/Models/IHasOwner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace AdvancedTodoList.Core.Models;

/// <summary>
/// An interface that represents an entity with an owner ID property.
/// </summary>
public interface IHasOwner
{
/// <summary>
/// Foreign key referencing the user who created this entity.
/// </summary>
public string? OwnerId { get; }
}
2 changes: 1 addition & 1 deletion AdvancedTodoList.Core/Models/TodoLists/TodoItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace AdvancedTodoList.Core.Models.TodoLists;
/// <summary>
/// Represents a to-do list item entity.
/// </summary>
public class TodoItem : IEntity<int>, ITodoListDependant
public class TodoItem : IEntity<int>, ITodoListDependant, IHasOwner
{
/// <summary>
/// An unique identifier for the to-do list item.
Expand Down
2 changes: 1 addition & 1 deletion AdvancedTodoList.Core/Models/TodoLists/TodoList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace AdvancedTodoList.Core.Models.TodoLists;
/// <summary>
/// Represents a to-do list entity.
/// </summary>
public class TodoList : IEntity<string>
public class TodoList : IEntity<string>, IHasOwner
{
/// <summary>
/// An unique identifier for the to-do list.
Expand Down
72 changes: 72 additions & 0 deletions AdvancedTodoList.Core/Services/Auth/IPermissionsChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using AdvancedTodoList.Core.Models;
using AdvancedTodoList.Core.Models.TodoLists.Members;

namespace AdvancedTodoList.Core.Services.Auth;

/// <summary>
/// Interface for a service that checks user's permissions.
/// </summary>
public interface IPermissionsChecker
{
/// <summary>
/// Asynchronously checks whether the user is a member of the to-do list with
/// specified ID.
/// </summary>
/// <param name="userId">ID of the user.</param>
/// <param name="todoListId">ID of the to-do list.</param>
/// <returns>
/// <see langword="true" /> if user is a member of the list; otherwise <see langword="false" />.
/// </returns>
Task<bool> IsMemberOfListAsync(string userId, string todoListId);

/// <summary>
/// Asynchronously checks whether the user is a member of the to-do list and
/// has a permission defined by the funciton <paramref name="permission"/>.
/// </summary>
/// <param name="userId">ID of the user.</param>
/// <param name="todoListId">ID of the to-do list.</param>
/// <param name="permission">Function that should return <see langword="true"/> if user has required permission.</param>
/// <returns>
/// <see langword="true" /> if user is a member of the list and has required permission;
/// otherwise <see langword="false" />.
/// </returns>
Task<bool> HasPermissionAsync(string userId, string todoListId, Func<RolePermissions, bool> permission);

/// <summary>
/// Asynchronously checks whether the user can touch an entity.
/// </summary>
/// <remarks>
/// This method firstly checks whether <paramref name="entity"/> implements <see cref="IHasOwner"/>
/// interface and if yes, checks if the user is the owner of the entity and is a member of the to-do list;
/// otherwise the method checks if user has the permission defined by the function <paramref name="permission"/>.
/// </remarks>
/// <typeparam name="TEntity">Type of the entity.</typeparam>
/// <typeparam name="TKey">Type of the unique identifier used by the entity.</typeparam>
/// <param name="userId">ID of the user whose permissions are achecked.</param>
/// <param name="todoListId">ID of the to-do list for which permission is checked.</param>
/// <param name="entity">ID of the entity.</param>
/// <param name="permission">Function that should return <see langword="true"/> if user has required permission.</param>
/// <returns>
/// <see langword="true"/> if user is either an owner of the entity and a member of a to-do list,
/// or he/she/they has permission defined by <paramref name="permission"/>; otherwise <see langword="false" />.
/// </returns>
Task<bool> CanTouchEntityAsync<TEntity, TKey>(string userId, string todoListId,
TEntity entity, Func<RolePermissions, bool> permission)
where TEntity : class, IEntity<TKey>
where TKey : IEquatable<TKey>;

/// <summary>
/// Asynchronously checks whether the user has a permission to change the role
/// defined by <paramref name="roleId"/>.
/// </summary>
/// <param name="userId">ID of the user.</param>
/// <param name="todoListId">ID of the to-do list for which permission is checked.</param>
/// <param name="roleId">ID of the role.</param>
/// <param name="permission">Function that should return <see langword="true"/> if user has required permission.</param>
/// <returns>
/// <see langword="true"/> if user has <paramref name="permission"/> and highest role priority than
/// the role defined by <paramref name="roleId"/>; otherwise <see langword="false" />.
/// </returns>
Task<bool> HasPermissionOverRoleAsync(string userId, string todoListId,
int roleId, Func<RolePermissions, bool> permission);
}
111 changes: 111 additions & 0 deletions AdvancedTodoList.Infrastructure/Services/Auth/PermissionsChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using AdvancedTodoList.Core.Dtos;
using AdvancedTodoList.Core.Models;
using AdvancedTodoList.Core.Models.TodoLists.Members;
using AdvancedTodoList.Core.Repositories;
using AdvancedTodoList.Core.Services.Auth;
using AdvancedTodoList.Infrastructure.Specifications;

namespace AdvancedTodoList.Infrastructure.Services.Auth;

/// <summary>
/// Service that checks user's permissions.
/// </summary>
public class PermissionsChecker(
ITodoListMembersRepository membersRepository,
IRepository<TodoListRole, int> rolesRepository) : IPermissionsChecker
{
private readonly ITodoListMembersRepository _membersRepository = membersRepository;
private readonly IRepository<TodoListRole, int> _rolesRepository = rolesRepository;

/// <summary>
/// Asynchronously checks whether the user is a member of the to-do list with
/// specified ID.
/// </summary>
/// <param name="userId">ID of the user.</param>
/// <param name="todoListId">ID of the to-do list.</param>
/// <returns>
/// <see langword="true" /> if user is a member of the list; otherwise <see langword="false" />.
/// </returns>
public async Task<bool> IsMemberOfListAsync(string userId, string todoListId)
{
return await _membersRepository.FindAsync(todoListId, userId) != null;
}

/// <summary>
/// Asynchronously checks whether the user is a member of the to-do list and
/// has a permission defined by the funciton <paramref name="permission"/>.
/// </summary>
/// <param name="userId">ID of the user.</param>
/// <param name="todoListId">ID of the to-do list.</param>
/// <param name="permission">Function that should return <see langword="true"/> if user has required permission.</param>
/// <returns>
/// <see langword="true" /> if user is a member of the list and has required permission;
/// otherwise <see langword="false" />.
/// </returns>
public async Task<bool> HasPermissionAsync(string userId, string todoListId, Func<RolePermissions, bool> permission)
{
MemberPermissionsSpecification specification = new(todoListId, userId);
var member = await _membersRepository.GetAggregateAsync<PermissionsAggregate>(specification);
// User is not a member or has no role - return false
if (member == null || member.Role == null) return false;

return permission(member.Role.Permissions);
}

/// <summary>
/// Asynchronously checks whether the user can touch an entity.
/// </summary>
/// <remarks>
/// This method firstly checks whether <paramref name="entity"/> implements <see cref="IHasOwner"/>
/// interface and if yes, checks if the user is the owner of the entity and is a member of the to-do list;
/// otherwise the method checks if user has the permission defined by the function <paramref name="permission"/>.
/// </remarks>
/// <typeparam name="TEntity">Type of the entity.</typeparam>
/// <typeparam name="TKey">Type of the unique identifier used by the entity.</typeparam>
/// <param name="userId">ID of the user whose permissions are achecked.</param>
/// <param name="todoListId">ID of the to-do list for which permission is checked.</param>
/// <param name="entity">ID of the entity.</param>
/// <param name="permission">Function that should return <see langword="true"/> if user has required permission.</param>
/// <returns>
/// <see langword="true"/> if user is either an owner of the entity and a member of a to-do list,
/// or he/she/they has permission defined by <paramref name="permission"/>; otherwise <see langword="false" />.
/// </returns>
Task<bool> IPermissionsChecker.CanTouchEntityAsync<TEntity, TKey>(string userId, string todoListId, TEntity entity, Func<RolePermissions, bool> permission)
{
// If user owns entity only check if he/she/they is member
if (entity is IHasOwner ownedEntity && ownedEntity.OwnerId == userId)
{
return IsMemberOfListAsync(userId, todoListId);
}
// Otherwise check if user has permission
return HasPermissionAsync(userId, todoListId, permission);
}

/// <summary>
/// Asynchronously checks whether the user is a member of the to-do list and
/// has a permission defined by the funciton <paramref name="permission"/>.
/// </summary>
/// <param name="userId">ID of the user.</param>
/// <param name="todoListId">ID of the to-do list.</param>
/// <param name="permission">Function that should return <see langword="true"/> if user has required permission.</param>
/// <returns>
/// <see langword="true" /> if user is a member of the list and has required permission;
/// otherwise <see langword="false" />.
/// </returns>
public async Task<bool> HasPermissionOverRoleAsync(string userId, string todoListId, int roleId, Func<RolePermissions, bool> permission)
{
MemberPermissionsSpecification specification = new(todoListId, userId);
var member = await _membersRepository.GetAggregateAsync<PermissionsAggregate>(specification);

// User is not a member, has no role or permission - return false
if (member == null || member.Role == null || !permission(member.Role.Permissions))
return false;

// Get other role
var role = await _rolesRepository.GetByIdAsync(roleId) ??
throw new ArgumentException("Role with 'roleId' was not found", nameof(roleId));

// Check if user has a higher priority
return member.Role.Priority < role.Priority;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using AdvancedTodoList.Core.Models.TodoLists.Members;
using AdvancedTodoList.Core.Specifications;
using AdvancedTodoList.Infrastructure.Services.Auth;
using System.Linq.Expressions;

namespace AdvancedTodoList.Infrastructure.Specifications;

/// <summary>
/// Represents a specification for obtaining an aggregate containg the to-do list member
/// and his/her/their role, used in <see cref="PermissionsChecker" />.
/// </summary>
/// <param name="todoListId">ID of the to-do list.</param>
/// <param name="userId">ID of the user.</param>
public class MemberPermissionsSpecification(string todoListId, string userId) : ISpecification<TodoListMember>
{
/// <summary>
/// Gets the to-do list ID.
/// </summary>
public string TodoListId { get; } = todoListId;
/// <summary>
/// Gets the user ID.
/// </summary>
public string UserId { get; } = userId;

/// <summary>
/// Gets the criteria expression that defines the filtering conditions.
/// </summary>
public Expression<Func<TodoListMember, bool>> Criteria =>
x => x.TodoListId == TodoListId && x.UserId == UserId;

/// <summary>
/// Gets the list of include expressions specifying a to-do list role to be included in the query results.
/// </summary>
public List<Expression<Func<TodoListMember, object?>>> Includes =>
[
x => x.Role
];

/// <summary>
/// Gets the list of include strings specifying related entities to be included in the query results.
/// </summary>
public List<string> IncludeStrings { get; } = [];
}
Loading

0 comments on commit 606a685

Please sign in to comment.