Skip to content

Commit

Permalink
RBAC - REST APIs (#2773)
Browse files Browse the repository at this point in the history
* GetAllRoles and GetRoleDetails(byID) with detailed correspondant actions APIs

* GetAllGroups and GetGroupByID fonctionnal APIs

* GetAllUsers and GetUserByID fonctionnal APIs

* GetGroupDetails API (list of members)

* some changes about the review

* New migrations and some changes in entities class of RBAC Model

* New migrations and some changes in entities class of RBAC Model (fixes)

* Add AccessControls gestion

* API Roles are functional

* RoleServices Changes

* delete useless comments

* document RoleController methods

* forgot dependencies

* save before resolve cache problems

* save

* Refact and upgrade quality of Roles API traitment

* add the actions role gesture

* Add AccessControl Get logic

* add Get,Create,Delete Routes for the APIs

* some change about EF dbContext and entities, Role, AccessControl and User logic implemented in services

* Migration (add Principal and his logic in EF DbContext and DB

* Migration + refactor the namespace of RBAC Services & move PredicateBuilder Extension into Crosscuting

* functionnal service for Users

* functionnal basic routes for group controller

* accessControl API Routes are functionnals (with Principal changes)

* fix mistake on delete AccessControl

* Improve quality of the Role service implementation (Exceptions, remove unused action...)

* fix some mistakes + add logger for Roles

* User logger

* logger AccessControl & group + fix somes bugs

* add accessControl in group and user details routes

* Add GetByName for User and group + some exceptions if name already exist.

* Tests for all RBAC repositories + RoleController

* Add tests of Groups & Users controllers

* Tests the AccessControl's Controller

* Review fixes

* Improving the robustness of tests (rbac)

* Testing RBAC Controller & Services + some modification in the correspondant files

* Refactor a mistake in namespace v10

* Some changes

* use autoFixture in all unitTest instead of creating some ressource manually

* Add tests of limits in methods

* remove forgotten comment

---------

Co-authored-by: Léo TUAILLON <leo.tuaillon@cgi.com>
  • Loading branch information
TLeoDev and Léo TUAILLON authored Jul 19, 2024
1 parent 7ca086b commit 1d9ee76
Show file tree
Hide file tree
Showing 171 changed files with 9,599 additions and 578 deletions.
34 changes: 34 additions & 0 deletions src/IoTHub.Portal.Application/Mappers/AccessControlProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) CGI France. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace IoTHub.Portal.Application.Mappers
{
using AutoMapper;
using IoTHub.Portal.Domain.Entities;
using IoTHub.Portal.Shared.Models.v10;

public class AccessControlProfile : Profile
{
public AccessControlProfile()
{
_ = CreateMap<AccessControl, AccessControlModel>()
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.PrincipalId, opts => opts.MapFrom(src => src.PrincipalId))
.ForMember(dest => dest.Scope, opts => opts.MapFrom(src => src.Scope))
.ForMember(dest => dest.Role, opts => opts.MapFrom(src => src.Role != null
? new RoleModel
{
Id = src.Role.Id,
Name = src.Role.Name
}
: null));

_ = CreateMap<AccessControlModel, AccessControl>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.PrincipalId, opts => opts.MapFrom(src => src.PrincipalId))
.ForMember(dest => dest.Scope, opts => opts.MapFrom(src => src.Scope))
.ForMember(dest => dest.RoleId, opts => opts.MapFrom(src => src.Role.Id))
.ForMember(dest => dest.Role, opts => opts.Ignore());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace IoTHub.Portal.Application.Mappers
using IoTHub.Portal.Domain.Entities;
using Microsoft.Azure.Devices.Shared;
using Models.v10.LoRaWAN;
using Shared.Models.v1._0;
using Shared.Models.v10;

public class ConcentratorProfile : Profile
{
Expand Down
39 changes: 39 additions & 0 deletions src/IoTHub.Portal.Application/Mappers/GroupProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) CGI France. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace IoTHub.Portal.Application.Mappers
{
using AutoMapper;
using IoTHub.Portal.Domain.Entities;
using IoTHub.Portal.Shared.Models.v10;

public class GroupProfile : Profile
{
public GroupProfile()
{
_ = CreateMap<Group, GroupModel>()
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
.ForMember(dest => dest.Color, opts => opts.MapFrom(src => src.Color))
.ForMember(dest => dest.PrincipalId, opts => opts.MapFrom(src => src.PrincipalId));

_ = CreateMap<Group, GroupDetailsModel>()
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
.ForMember(dest => dest.Color, opts => opts.MapFrom(src => src.Color))
.ForMember(dest => dest.Description, opts => opts.MapFrom(src => src.Description))
.ForMember(dest => dest.PrincipalId, opts => opts.MapFrom(src => src.PrincipalId));

_ = CreateMap<GroupModel, Group>()
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
.ForMember(dest => dest.Color, opts => opts.MapFrom(src => src.Color));

_ = CreateMap<GroupDetailsModel, Group>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.PrincipalId, opt => opt.Ignore())
.ForMember(dest => dest.Color, opt => opt.MapFrom(src => src.Color))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description));
}
}
}
41 changes: 41 additions & 0 deletions src/IoTHub.Portal.Application/Mappers/RoleProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) CGI France. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace IoTHub.Portal.Application.Mappers
{
using AutoMapper;
using IoTHub.Portal.Domain.Entities;
using IoTHub.Portal.Shared.Models.v10;

public class RoleProfile : Profile
{
public RoleProfile()
{
_ = CreateMap<Role, RoleModel>()
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
.ForMember(dest => dest.Color, opts => opts.MapFrom(src => src.Color));

_ = CreateMap<RoleModel, Role>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
.ForMember(dest => dest.Color, opts => opts.MapFrom(src => src.Color));

_ = CreateMap<Role, RoleDetailsModel>()
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
.ForMember(dest => dest.Color, opts => opts.MapFrom(src => src.Color))
.ForMember(dest => dest.Description, opts => opts.MapFrom(src => src.Description))
.ForMember(dest => dest.Actions, opts => opts.MapFrom(src =>
src.Actions.Select(a => a.Name)));

_ = CreateMap<RoleDetailsModel, Role>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
.ForMember(dest => dest.Color, opts => opts.MapFrom(src => src.Color))
.ForMember(dest => dest.Description, opts => opts.MapFrom(src => src.Description))
.ForMember(dest => dest.Actions, opts => opts.MapFrom(src =>
src.Actions.Select(a => new Action { Name = a })));
}
}
}
45 changes: 45 additions & 0 deletions src/IoTHub.Portal.Application/Mappers/UserProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) CGI France. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace IoTHub.Portal.Application.Mappers
{
using AutoMapper;
using IoTHub.Portal.Domain.Entities;
using IoTHub.Portal.Shared.Models.v10;

public class UserProfile : Profile
{
public UserProfile()
{
_ = CreateMap<User, UserModel>()
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.GivenName, opts => opts.MapFrom(src => src.GivenName))
.ForMember(dest => dest.Email, opts => opts.MapFrom(src => src.Email))
.ForMember(dest => dest.PrincipalId, opts => opts.MapFrom(src => src.PrincipalId));

_ = CreateMap<User, UserDetailsModel>()
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
.ForMember(dest => dest.Email, opts => opts.MapFrom(src => src.Email))
.ForMember(dest => dest.GivenName, opts => opts.MapFrom(src => src.GivenName))
.ForMember(dest => dest.FamilyName, opts => opts.MapFrom(src => src.FamilyName))
.ForMember(dest => dest.Avatar, opts => opts.MapFrom(src => src.Avatar))
.ForMember(dest => dest.PrincipalId, opts => opts.MapFrom(src => src.PrincipalId));

_ = CreateMap<UserDetailsModel, User>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Avatar, opts => opts.MapFrom(src => src.Avatar))
.ForMember(dest => dest.GivenName, opt => opt.MapFrom(src => src.GivenName))
.ForMember(dest => dest.FamilyName, opt => opt.MapFrom(src => src.FamilyName));

_ = CreateMap<UserModel, User>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
.ForMember(dest => dest.GivenName, opt => opt.MapFrom(src => src.GivenName))
.ForMember(dest => dest.PrincipalId, opt => opt.MapFrom(src => src.PrincipalId));
}
}
}
155 changes: 155 additions & 0 deletions src/IoTHub.Portal.Application/Services/AccessControlService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright (c) CGI France. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace IoTHub.Portal.Application.Services
{
using System.Threading.Tasks;
using AutoMapper;
using IoTHub.Portal.Domain;
using IoTHub.Portal.Domain.Entities;
using IoTHub.Portal.Domain.Repositories;
using IoTHub.Portal.Shared.Models.v10;
using IoTHub.Portal.Shared.Models.v10.Filters;
using IoTHub.Portal.Domain.Exceptions;
using IoTHub.Portal.Crosscutting;

internal class AccessControlService : IAccessControlManagementService
{
private readonly IAccessControlRepository accessControlRepository;
private readonly IUnitOfWork unitOfWork;
private readonly IMapper mapper;
private readonly IRoleRepository roleRepository;
private readonly IPrincipalRepository principalRepository;

public AccessControlService(IAccessControlRepository accessControlRepository, IUnitOfWork unitOfWork, IMapper mapper, IRoleRepository roleRepository, IPrincipalRepository principalRepository)
{
this.accessControlRepository = accessControlRepository;
this.unitOfWork = unitOfWork;
this.mapper = mapper;
this.roleRepository = roleRepository;
this.principalRepository = principalRepository;
}

public async Task<AccessControlModel> GetAccessControlAsync(string Id)
{
var acEntity = await this.accessControlRepository.GetByIdAsync(Id, ac => ac.Role);
if (acEntity is null)
{
throw new ResourceNotFoundException($"The AccessControl with the id {Id} doesn't exist");
}
var acModel = this.mapper.Map<AccessControlModel>(acEntity);
return acModel;
}

public async Task<PaginatedResult<AccessControlModel>> GetAccessControlPage(
string? searchKeyword = null,
int pageSize = 10,
int pageNumber = 0,
string[] orderBy = null,
string? principalId = null
)
{
var acFilter = new AccessControlFilter
{
Keyword = searchKeyword,
PageSize = pageSize,
PageNumber = pageNumber,
OrderBy = orderBy
};
var acPredicate = PredicateBuilder.True<AccessControl>();
if (!string.IsNullOrWhiteSpace(acFilter.Keyword))
{
acPredicate = acPredicate.And(ac => ac.Scope.ToLower().Contains(acFilter.Keyword.ToLower()) || ac.Role.Name.ToLower().Contains(acFilter.Keyword.ToLower()));
}

if (!string.IsNullOrEmpty(principalId))
{
acPredicate = acPredicate.And(ac => ac.PrincipalId == principalId);
}

var paginatedAc = await this.accessControlRepository.GetPaginatedListAsync(
pageNumber,
pageSize,
orderBy,
acPredicate
);

var paginatedAcDto = new PaginatedResult<AccessControlModel>
{
Data = paginatedAc.Data.Select(x => this.mapper.Map<AccessControlModel>(x)).ToList(),
TotalCount = paginatedAc.TotalCount,
CurrentPage = paginatedAc.CurrentPage,
PageSize = pageSize
};
return new PaginatedResult<AccessControlModel>(paginatedAcDto.Data, paginatedAcDto.TotalCount);
}

public async Task<AccessControlModel> CreateAccessControl(AccessControlModel accessControl)
{
if (accessControl is null)
{
throw new ArgumentNullException(nameof(accessControl));
}
var principal = await this.principalRepository.GetByIdAsync(accessControl.PrincipalId);
if (principal == null)
{
throw new ResourceNotFoundException($"The principal with the id {accessControl.PrincipalId} does'nt exist !");
}
var role = await this.roleRepository.GetByIdAsync(accessControl.Role.Id);
if (role == null)
{
throw new ResourceNotFoundException($"The role {accessControl.Role.Name} with the id {accessControl.Role.Id} does'nt exist !");
}
var acEntity = this.mapper.Map<AccessControl>(accessControl);
await this.accessControlRepository.InsertAsync(acEntity);
await this.unitOfWork.SaveAsync();

var createdAc = await this.accessControlRepository.GetByIdAsync(acEntity.Id, ac => ac.Role);
var createdModel = this.mapper.Map<AccessControlModel>(createdAc);
return createdModel;

}
public async Task<AccessControlModel?> UpdateAccessControl(string id, AccessControlModel accessControl)
{
if (accessControl is null)
{
throw new ArgumentNullException(nameof(accessControl));
}
var acEntity = await this.accessControlRepository.GetByIdAsync(id, ac => ac.Role);
if (acEntity is null)
{
throw new ResourceNotFoundException($"The AccessControl with the id {id} doesn't exist");
}
var principal = await this.principalRepository.GetByIdAsync(accessControl.PrincipalId);
if (principal is null)
{
throw new ResourceNotFoundException($"The principal with the id {accessControl.PrincipalId} not found !");
}
var role = await this.roleRepository.GetByIdAsync(accessControl.Role.Id);
if (role is null)
{
throw new ResourceNotFoundException($"Specified role with the id {accessControl.Role.Id} not found");
}
acEntity.PrincipalId = accessControl.PrincipalId;
acEntity.RoleId = accessControl.Role.Id;
acEntity.Scope = accessControl.Scope;
accessControlRepository.Update(acEntity);
await this.unitOfWork.SaveAsync();

var createdAc = await this.accessControlRepository.GetByIdAsync(id, ac => ac.Role);
return this.mapper.Map<AccessControlModel>(createdAc);
}

public async Task<bool> DeleteAccessControl(string id)
{
var acEntity = await this.accessControlRepository.GetByIdAsync(id);
if (acEntity is null)
{
throw new ResourceNotFoundException($"The AccessControl with the id {id} doesn't exist");
}
accessControlRepository.Delete(id);
await this.unitOfWork.SaveAsync();
return true;
}
}
}
Loading

0 comments on commit 1d9ee76

Please sign in to comment.