merge: bring documentation baseline from docs/master into docs/rc-1.0.2
This commit is contained in:
@@ -8,21 +8,27 @@ namespace adas_core.Authentication.Attributes;
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class AuthorizeRolesAttribute(PermissionEnum.RolesType type) : Attribute, IAuthorizationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs authorization by ensuring the current user is authenticated and has a role matching the required permission type.
|
||||
/// Responds with an unauthorized result when no user is authenticated, bypasses role checking for the AuthSome permission type,
|
||||
/// and otherwise responds with a forbid result when no user role starts with the permission type value.
|
||||
/// </summary>
|
||||
/// <param name="context">The authorization filter context providing access to the HTTP context and where the authorization result is assigned.</param>
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
var user = context.HttpContext.User;
|
||||
|
||||
if (user.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
context.Result = new UnauthorizedResult();
|
||||
return;
|
||||
var user = context.HttpContext.User;
|
||||
|
||||
if (user.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
context.Result = new UnauthorizedResult();
|
||||
return;
|
||||
}
|
||||
|
||||
var roles = user.Claims.Where(c => c.Type == ClaimTypes.Role);
|
||||
|
||||
if (type == PermissionEnum.RolesType.AuthSome) return;
|
||||
|
||||
var hasRequiredRole = roles.Any(r => r.Value.StartsWith(type.ToString()));
|
||||
if (!hasRequiredRole) context.Result = new ForbidResult();
|
||||
}
|
||||
|
||||
var roles = user.Claims.Where(c => c.Type == ClaimTypes.Role);
|
||||
|
||||
if (type == PermissionEnum.RolesType.AuthSome) return;
|
||||
|
||||
var hasRequiredRole = roles.Any(r => r.Value.StartsWith(type.ToString()));
|
||||
if (!hasRequiredRole) context.Result = new ForbidResult();
|
||||
}
|
||||
}
|
||||
@@ -8,22 +8,26 @@ public class AuthorizePermissionsAttribute(
|
||||
IUserService userService)
|
||||
: Attribute, IAuthorizationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles authorization by validating the presence of the <c>Displayid</c> request header and ensuring the authenticated user exists in the user service. If the header is missing or the user cannot be found, the request is short-circuited with a <see cref="NotFoundObjectResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="AuthorizationFilterContext"/> for the current request, providing access to the HTTP context, user identity, and the ability to set the action result.</param>
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
var user = context.HttpContext.User;
|
||||
|
||||
var userName = user.Identity?.Name ?? string.Empty;
|
||||
var displayIdHeader = context.HttpContext.Request.Headers["Displayid"];
|
||||
|
||||
if (string.IsNullOrEmpty(displayIdHeader.ToString()))
|
||||
{
|
||||
context.Result = new NotFoundObjectResult("displayIdHeader not found reference id doesn't exist on header");
|
||||
return;
|
||||
var user = context.HttpContext.User;
|
||||
|
||||
var userName = user.Identity?.Name ?? string.Empty;
|
||||
var displayIdHeader = context.HttpContext.Request.Headers["Displayid"];
|
||||
|
||||
if (string.IsNullOrEmpty(displayIdHeader.ToString()))
|
||||
{
|
||||
context.Result = new NotFoundObjectResult("displayIdHeader not found reference id doesn't exist on header");
|
||||
return;
|
||||
}
|
||||
|
||||
var userDao = userService.GetUserByUserName(userName).Result;
|
||||
|
||||
if (userDao == null)
|
||||
context.Result = new NotFoundObjectResult("User not found on AuthorizePermissionsAttribute");
|
||||
}
|
||||
|
||||
var userDao = userService.GetUserByUserName(userName).Result;
|
||||
|
||||
if (userDao == null)
|
||||
context.Result = new NotFoundObjectResult("User not found on AuthorizePermissionsAttribute");
|
||||
}
|
||||
}
|
||||
@@ -12,87 +12,94 @@ namespace adas_core.Authentication.Attributes;
|
||||
public class PermissionAuthorizeAttribute(string source, string? resourceIdHeader = null)
|
||||
: AuthorizeAttribute, IAsyncAuthorizationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs asynchronous authorization by validating the user's roles, username, and source header against the permission service.
|
||||
/// For the <c>Panel</c> source, it verifies that at least one of the user's roles grants access to the panel; for other sources, it requires a resource identifier header (such as <c>DisplayId</c> or <c>UnitId</c>) and checks access to the corresponding resource.
|
||||
/// If any required value is missing or no role grants the needed permission, the response is set to <see cref="ForbidResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="AuthorizationFilterContext"/> for the current authorization request, providing access to the HTTP context, services, and the result to set when authorization fails.</param>
|
||||
/// <exception cref="ForbbidenException">Thrown when the <see cref="IPermissionService"/> cannot be resolved from the request services.</exception>
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
// Obtener el servicio de permisos.
|
||||
var permissionService = context.HttpContext.RequestServices.GetService<IPermissionService>()
|
||||
?? throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission);
|
||||
|
||||
var user = context.HttpContext.User;
|
||||
|
||||
// Obtener el header que indica la fuente.
|
||||
var headerSource = context.HttpContext.Request.Headers[source].FirstOrDefault();
|
||||
|
||||
// Obtener el identificador del usuario
|
||||
var username = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
// Obtiene la lista de roles (valores) del usuario.
|
||||
var userRoles = user.Claims
|
||||
.Where(c => c.Type == ClaimTypes.Role)
|
||||
.Select(c => c.Value)
|
||||
.ToList();
|
||||
|
||||
// Procesa cada rol: si contiene 2 guiones bajos, elimina el tercer segmento.
|
||||
var processedRoles = userRoles.Select(role =>
|
||||
{
|
||||
var parts = role.Split('_');
|
||||
return parts.Length == 2 ? parts[0] : role;
|
||||
}).ToList();
|
||||
|
||||
// Validar que se tenga al menos un rol, username y headerSource.
|
||||
if (!processedRoles.Any() || string.IsNullOrEmpty(username) || string.IsNullOrEmpty(headerSource))
|
||||
{
|
||||
context.Result = new ForbidResult();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parseamos el header para obtener la fuente.
|
||||
var source1 =
|
||||
(PermissionEnum.SourcePermissionsEnum)Enum.Parse(typeof(PermissionEnum.SourcePermissionsEnum),
|
||||
headerSource);
|
||||
|
||||
var hasPermission = false;
|
||||
|
||||
if (source1 == PermissionEnum.SourcePermissionsEnum.Panel)
|
||||
{
|
||||
// Si la fuente es PANEL, verificamos que alguno de los roles tenga acceso.
|
||||
foreach (var roleStr in processedRoles)
|
||||
// Obtener el servicio de permisos.
|
||||
var permissionService = context.HttpContext.RequestServices.GetService<IPermissionService>()
|
||||
?? throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission);
|
||||
|
||||
var user = context.HttpContext.User;
|
||||
|
||||
// Obtener el header que indica la fuente.
|
||||
var headerSource = context.HttpContext.Request.Headers[source].FirstOrDefault();
|
||||
|
||||
// Obtener el identificador del usuario
|
||||
var username = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
// Obtiene la lista de roles (valores) del usuario.
|
||||
var userRoles = user.Claims
|
||||
.Where(c => c.Type == ClaimTypes.Role)
|
||||
.Select(c => c.Value)
|
||||
.ToList();
|
||||
|
||||
// Procesa cada rol: si contiene 2 guiones bajos, elimina el tercer segmento.
|
||||
var processedRoles = userRoles.Select(role =>
|
||||
{
|
||||
if (!Enum.TryParse(typeof(PermissionEnum.RolesType), roleStr, out var roleObj)) continue;
|
||||
var role = (PermissionEnum.RolesType)roleObj;
|
||||
if (!await permissionService.HasAccessToPanel(username, role, source1)) continue;
|
||||
hasPermission = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasPermission) context.Result = new ForbidResult();
|
||||
}
|
||||
else if (resourceIdHeader != null)
|
||||
{
|
||||
// Para otras fuentes, se requiere un header que indique el resourceId.
|
||||
var resourceId = context.HttpContext.Request.Headers[resourceIdHeader].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(resourceId))
|
||||
var parts = role.Split('_');
|
||||
return parts.Length == 2 ? parts[0] : role;
|
||||
}).ToList();
|
||||
|
||||
// Validar que se tenga al menos un rol, username y headerSource.
|
||||
if (!processedRoles.Any() || string.IsNullOrEmpty(username) || string.IsNullOrEmpty(headerSource))
|
||||
{
|
||||
context.Result = new ForbidResult();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar que al menos uno de los roles tenga acceso al recurso.
|
||||
foreach (var roleStr in processedRoles)
|
||||
if (Enum.TryParse(typeof(PermissionEnum.RolesType), roleStr, out var roleObj))
|
||||
|
||||
// Parseamos el header para obtener la fuente.
|
||||
var source1 =
|
||||
(PermissionEnum.SourcePermissionsEnum)Enum.Parse(typeof(PermissionEnum.SourcePermissionsEnum),
|
||||
headerSource);
|
||||
|
||||
var hasPermission = false;
|
||||
|
||||
if (source1 == PermissionEnum.SourcePermissionsEnum.Panel)
|
||||
{
|
||||
// Si la fuente es PANEL, verificamos que alguno de los roles tenga acceso.
|
||||
foreach (var roleStr in processedRoles)
|
||||
{
|
||||
if (!Enum.TryParse(typeof(PermissionEnum.RolesType), roleStr, out var roleObj)) continue;
|
||||
var role = (PermissionEnum.RolesType)roleObj;
|
||||
hasPermission = resourceIdHeader switch
|
||||
{
|
||||
"DisplayId" => await permissionService.HasAccessToDisplay(username, role, source1, resourceId),
|
||||
"UnitId" => await permissionService.HasAccessToUnit(username, role, source1, resourceId),
|
||||
_ => false
|
||||
};
|
||||
if (hasPermission)
|
||||
break;
|
||||
if (!await permissionService.HasAccessToPanel(username, role, source1)) continue;
|
||||
hasPermission = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasPermission) context.Result = new ForbidResult();
|
||||
|
||||
if (!hasPermission) context.Result = new ForbidResult();
|
||||
}
|
||||
else if (resourceIdHeader != null)
|
||||
{
|
||||
// Para otras fuentes, se requiere un header que indique el resourceId.
|
||||
var resourceId = context.HttpContext.Request.Headers[resourceIdHeader].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(resourceId))
|
||||
{
|
||||
context.Result = new ForbidResult();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar que al menos uno de los roles tenga acceso al recurso.
|
||||
foreach (var roleStr in processedRoles)
|
||||
if (Enum.TryParse(typeof(PermissionEnum.RolesType), roleStr, out var roleObj))
|
||||
{
|
||||
var role = (PermissionEnum.RolesType)roleObj;
|
||||
hasPermission = resourceIdHeader switch
|
||||
{
|
||||
"DisplayId" => await permissionService.HasAccessToDisplay(username, role, source1, resourceId),
|
||||
"UnitId" => await permissionService.HasAccessToUnit(username, role, source1, resourceId),
|
||||
_ => false
|
||||
};
|
||||
if (hasPermission)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasPermission) context.Result = new ForbidResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,23 +14,36 @@ public class AuthorityService(
|
||||
ILocalAuditService auditService)
|
||||
: IAuthorityService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new authority for the specified user with the given role name by delegating to the authority repository.
|
||||
/// </summary>
|
||||
/// <param name="roleName">The name of the role to associate with the authority.</param>
|
||||
/// <param name="userId">The unique identifier of the user for whom the authority is being created.</param>
|
||||
public void CreateNew(string roleName, ObjectId userId)
|
||||
{
|
||||
authorityRepository.CreateNewAuthority(roleName, userId);
|
||||
}
|
||||
{
|
||||
authorityRepository.CreateNewAuthority(roleName, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a new <paramref name="authorization"/> into the authority repository and records an audit log entry for the operation using the current HTTP context user.
|
||||
/// </summary>
|
||||
/// <param name="authorization">The authorization entity to be inserted and tracked in the audit log.</param>
|
||||
public async Task InsertOne(Authorization authorization)
|
||||
{
|
||||
await authorityRepository.InsertOneAsync(authorization);
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, authorization);
|
||||
}
|
||||
{
|
||||
await authorityRepository.InsertOneAsync(authorization);
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, authorization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing authorization record in the repository and records an audit log entry capturing the previous and updated states for the current user.
|
||||
/// </summary>
|
||||
/// <param name="authorization">The authorization entity containing the identifier of the record to update and its new values.</param>
|
||||
public async Task updateOne(Authorization authorization)
|
||||
{
|
||||
var oldAuth = await authorityRepository.GetById(authorization.Id);
|
||||
await authorityRepository.UpdateOneAsync(authorization.Id, authorization);
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAuth, authorization);
|
||||
}
|
||||
{
|
||||
var oldAuth = await authorityRepository.GetById(authorization.Id);
|
||||
await authorityRepository.UpdateOneAsync(authorization.Id, authorization);
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAuth, authorization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the authorities of the specified user.
|
||||
@@ -42,67 +55,93 @@ public class AuthorityService(
|
||||
return authorityRepository.GetUserAuthorities(userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all available authorities from the underlying repository.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation, containing a list of all <see cref="Authorization"/> records.</returns>
|
||||
public Task<List<Authorization>> GetAllAuthorities()
|
||||
{
|
||||
return authorityRepository.GetAllAuthorities();
|
||||
}
|
||||
{
|
||||
return authorityRepository.GetAllAuthorities();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all authorities associated with the specified user and records an audit log entry for each removed authority. Returns <c>false</c> if no authorities were deleted or if an exception is encountered during processing.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the user whose authorities should be deleted.</param>
|
||||
/// <returns><c>true</c> if the authorities were successfully deleted; otherwise, <c>false</c>.</returns>
|
||||
public async Task<bool> DeleteAuthoritiesForUser(ObjectId id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await authorityRepository.GetUserAuthorities(id);
|
||||
var result = await authorityRepository.DeleteAllAuthoritiesByUser(id);
|
||||
if (!result) return result;
|
||||
|
||||
foreach (var auth in list)
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, auth, null);
|
||||
return result;
|
||||
try
|
||||
{
|
||||
var list = await authorityRepository.GetUserAuthorities(id);
|
||||
var result = await authorityRepository.DeleteAllAuthoritiesByUser(id);
|
||||
if (!result) return result;
|
||||
|
||||
foreach (var auth in list)
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, auth, null);
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("Exception deleting authorities for user {id}. Exception: {e}", id, e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("Exception deleting authorities for user {id}. Exception: {e}", id, e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new user authority by persisting the provided authorization record and writing an audit log entry for the operation.
|
||||
/// </summary>
|
||||
/// <param name="authorization">The authorization entity to be created and stored.</param>
|
||||
/// <returns>The created <see cref="Authorization"/> entity.</returns>
|
||||
public async Task<Authorization?> CreateNewUserAuthority(Authorization authorization)
|
||||
{
|
||||
await authorityRepository.InsertOneAsync(authorization);
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, authorization);
|
||||
return authorization;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteUserAuthority(ObjectId userAuthorityIdParsed)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await authorityRepository.DeleteAsync(userAuthorityIdParsed);
|
||||
if (result != null)
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, result, null);
|
||||
return result != null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("Exception deleting user authority {userAuthorityIdParsed}. Exception: {e}",
|
||||
userAuthorityIdParsed, e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Authorization?> EditUserAuthority(Authorization authorization)
|
||||
{
|
||||
try
|
||||
{
|
||||
var oldAuth = await authorityRepository.GetById(authorization.Id);
|
||||
await authorityRepository.UpdateOneAsync(authorization.Id, authorization);
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAuth, authorization);
|
||||
await authorityRepository.InsertOneAsync(authorization);
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, authorization);
|
||||
return authorization;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a user authority by its parsed identifier and records an audit log entry when the deletion succeeds.
|
||||
/// Returns <c>true</c> if the authority was found and removed, and <c>false</c> when the entity is not found or an exception occurs during processing.
|
||||
/// </summary>
|
||||
/// <param name="userAuthorityIdParsed">The parsed identifier of the user authority to delete.</param>
|
||||
/// <returns><c>true</c> if the user authority was successfully deleted; otherwise, <c>false</c>.</returns>
|
||||
public async Task<bool> DeleteUserAuthority(ObjectId userAuthorityIdParsed)
|
||||
{
|
||||
Log.Error("Exception editing user authority {authorization}. Exception: {e}", authorization, e.Message);
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var result = await authorityRepository.DeleteAsync(userAuthorityIdParsed);
|
||||
if (result != null)
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, result, null);
|
||||
return result != null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("Exception deleting user authority {userAuthorityIdParsed}. Exception: {e}",
|
||||
userAuthorityIdParsed, e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edits an existing user authority, records the change in an audit log, and returns the updated authorization.
|
||||
/// Returns <c>null</c> if the update operation fails due to an exception.
|
||||
/// </summary>
|
||||
/// <param name="authorization">The authorization entity containing the updated information to persist.</param>
|
||||
/// <returns>The updated <see cref="Authorization"/> on success, or <c>null</c> if an error occurs during the operation.</returns>
|
||||
public async Task<Authorization?> EditUserAuthority(Authorization authorization)
|
||||
{
|
||||
try
|
||||
{
|
||||
var oldAuth = await authorityRepository.GetById(authorization.Id);
|
||||
await authorityRepository.UpdateOneAsync(authorization.Id, authorization);
|
||||
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAuth, authorization);
|
||||
return authorization;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("Exception editing user authority {authorization}. Exception: {e}", authorization, e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,58 @@ namespace adas_core.Authentication.Interfaces;
|
||||
|
||||
public interface IAuthorityService
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves the list of authorizations associated with the specified user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The unique identifier of the user whose authorizations are being requested.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Authorization"/> objects for the user.</returns>
|
||||
public Task<List<Authorization>> GetUserAuthorities(ObjectId userId);
|
||||
/// <summary>
|
||||
/// Creates a new role with the specified name and associates it with the given user.
|
||||
/// </summary>
|
||||
/// <param name="roleName">The name of the role to create.</param>
|
||||
/// <param name="userId">The identifier of the user to associate with the new role.</param>
|
||||
public void CreateNew(string roleName, ObjectId userId);
|
||||
/// <summary>
|
||||
/// Inserts a new authorization record into the data store.
|
||||
/// </summary>
|
||||
/// <param name="authorization">The authorization entity to insert.</param>
|
||||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||||
public Task InsertOne(Authorization authorization);
|
||||
/// <summary>
|
||||
/// Updates a single authorization record.
|
||||
/// </summary>
|
||||
/// <param name="authorization">The authorization object containing the data to be updated.</param>
|
||||
public Task updateOne(Authorization authorization);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves all authorizations.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains a list of all authorization objects.</returns>
|
||||
public Task<List<Authorization>> GetAllAuthorities();
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all authorities associated with the user identified by the specified id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the user whose authorities should be removed.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains <c>true</c> if authorities were deleted; otherwise, <c>false</c>.</returns>
|
||||
Task<bool> DeleteAuthoritiesForUser(ObjectId id);
|
||||
/// <summary>
|
||||
/// Creates a new user authority based on the provided <paramref name="authorization"/>.
|
||||
/// </summary>
|
||||
/// <param name="authorization">The authorization data used to create the new user authority.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the created <see cref="Authorization"/>, or <c>null</c> if the authority could not be created.</returns>
|
||||
Task<Authorization?> CreateNewUserAuthority(Authorization authorization);
|
||||
/// <summary>
|
||||
/// Asynchronously deletes a user authority identified by the provided parsed identifier.
|
||||
/// </summary>
|
||||
/// <param name="userAuthorityIdParsed">The parsed identifier of the user authority to delete.</param>
|
||||
/// <returns>A task that represents the asynchronous delete operation. The task result is <see langword="true"/> if the user authority was successfully deleted; otherwise, <see langword="false"/>.</returns>
|
||||
Task<bool> DeleteUserAuthority(ObjectId userAuthorityIdParsed);
|
||||
/// <summary>
|
||||
/// Edits the user authority using the provided authorization data.
|
||||
/// </summary>
|
||||
/// <param name="authorization">The authorization object containing the user authority details to be updated.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the updated <see cref="Authorization"/>, or <c>null</c> if the authorization was not found.</returns>
|
||||
Task<Authorization?> EditUserAuthority(Authorization authorization);
|
||||
}
|
||||
@@ -9,13 +9,49 @@ public interface ILoginService
|
||||
{
|
||||
bool AllowPassword { get; }
|
||||
UserEnum.LoginMethod Method { get; }
|
||||
/// <summary>
|
||||
/// Authenticates a user with the provided credentials and returns the corresponding user account.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the user attempting to log in.</param>
|
||||
/// <param name="password">The password associated with the username.</param>
|
||||
/// <returns>A <see cref="Task{User}"/> representing the asynchronous operation, containing the authenticated <see cref="User"/>.</returns>
|
||||
Task<User> Login(string username, string password);
|
||||
/// <summary>
|
||||
/// Authenticates a user based on the provided HTTP context.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context containing the request information used to perform the login.</param>
|
||||
/// <returns>A task that represents the asynchronous login operation. The task result contains the authenticated <see cref="User"/>.</returns>
|
||||
Task<User> Login(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates a user based on the provided credentials.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the user to authenticate.</param>
|
||||
/// <param name="password">The password of the user to authenticate.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the authenticated <see cref="User"/>.</returns>
|
||||
Task<User> Authenticate(string username, string password);
|
||||
/// <summary>
|
||||
/// Retrieves a user by their unique identifier, returning null if no matching user is found.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the user to look up.</param>
|
||||
/// <returns>The user with the specified identifier, or null if no user is found.</returns>
|
||||
Task<User?> GetById(ObjectId id);
|
||||
/// <summary>
|
||||
/// Retrieves a user from the data store by their email address, or returns null if no matching user is found.
|
||||
/// </summary>
|
||||
/// <param name="email">The email address used to look up the user.</param>
|
||||
/// <returns>A task that resolves to the <see cref="User"/> matching the provided email, or null if no user is found.</returns>
|
||||
Task<User?> GetByEmail(string email);
|
||||
/// <summary>
|
||||
/// Retrieves a user by their username asynchronously, returning <c>null</c> if no matching user is found.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to look up.</param>
|
||||
/// <returns>A <see cref="Task{User}"/> that resolves to the matching <see cref="User"/>, or <c>null</c> if no user exists with the specified username.</returns>
|
||||
Task<User?> GetByUsername(string username);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves a list of all users in the system.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation, containing a list of all <see cref="User"/> entities.</returns>
|
||||
Task<List<User>> GetAllUsers();
|
||||
}
|
||||
@@ -6,9 +6,25 @@ namespace adas_core.Authentication.Interfaces;
|
||||
public interface ITokenService
|
||||
{
|
||||
int RefreshTokenValidityInDays { get; }
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="SecurityToken"/> associated with the specified username and claims.
|
||||
/// Returns <see langword="null"/> when no matching token is found.
|
||||
/// </summary>
|
||||
/// <param name="username">The username used to look up the security token.</param>
|
||||
/// <param name="claims">The list of claims associated with the token.</param>
|
||||
/// <returns>A <see cref="SecurityToken"/> if a matching token is found; otherwise, <see langword="null"/>.</returns>
|
||||
SecurityToken? GetToken(string username, List<Claim> claims);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the current refresh token used to obtain new access tokens for authentication.
|
||||
/// </summary>
|
||||
/// <returns>The refresh token as a string.</returns>
|
||||
string GetRefreshToken();
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the specified security token into its string representation.
|
||||
/// </summary>
|
||||
/// <param name="token">The security token to serialize.</param>
|
||||
/// <returns>A string containing the serialized form of the security token.</returns>
|
||||
string Serialize(SecurityToken token);
|
||||
}
|
||||
@@ -14,30 +14,159 @@ public interface IUserService
|
||||
static readonly string TokenTypeUser = "user";
|
||||
static readonly string TokenTypeRefresh = "refresh";
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates a user with the provided credentials and returns a token result upon successful login.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the user attempting to log in.</param>
|
||||
/// <param name="password">The password associated with the specified username.</param>
|
||||
/// <returns>A task that represents the asynchronous login operation, containing the <see cref="TokenResult"/> with the authentication token information.</returns>
|
||||
Task<TokenResult> Login(string username, string password);
|
||||
/// <summary>
|
||||
/// Authenticates a user based on the provided username and password and returns the corresponding user.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the user to authenticate.</param>
|
||||
/// <param name="password">The password of the user to authenticate.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the authenticated <see cref="User"/>.</returns>
|
||||
Task<User> GetUser(string username, string password);
|
||||
/// <summary>
|
||||
/// Refreshes an authentication token using the provided refresh token and returns the resulting token information.
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">The refresh token used to obtain a new access token.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous operation, containing the <see cref="TokenResult"/> with the refreshed token details.</returns>
|
||||
Task<TokenResult> RefreshToken(string refreshToken);
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates a user by exchanging a provided access token for a login result.
|
||||
/// </summary>
|
||||
/// <param name="token">The access token used to authenticate the user.</param>
|
||||
/// <returns>A task that represents the asynchronous login operation, containing the <see cref="TokenResult"/> with the outcome of the authentication.</returns>
|
||||
Task<TokenResult> LoginWithAccessToken(string token);
|
||||
/// <summary>
|
||||
/// Validates the specified token according to its type and outputs the corresponding <see cref="SecurityToken"/>.
|
||||
/// </summary>
|
||||
/// <param name="token">The token string to be validated.</param>
|
||||
/// <param name="tokenType">The type of the token, used to determine the appropriate validation strategy.</param>
|
||||
/// <param name="securityToken">When the method returns, contains the validated <see cref="SecurityToken"/> if validation succeeds.</param>
|
||||
void ValidateToken(string token, string tokenType, out SecurityToken securityToken);
|
||||
/// <summary>
|
||||
/// Retrieves the user associated with the provided JWT security token.
|
||||
/// Returns <c>null</c> if the token is <c>null</c> or if no matching user is found.
|
||||
/// </summary>
|
||||
/// <param name="jwtToken">The JWT security token used to identify the user. May be <c>null</c>.</param>
|
||||
/// <returns>A task that resolves to the <see cref="User"/> associated with the token, or <c>null</c> if the token is invalid or no user matches.</returns>
|
||||
Task<User?> GetUserByToken(JwtSecurityToken? jwtToken);
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves a user by validating a CAS (Central Authentication Service) ticket against the specified service.
|
||||
/// </summary>
|
||||
/// <param name="service">The service URL or identifier that the ticket was issued for and must be validated against.</param>
|
||||
/// <param name="ticket">The CAS ticket string used to authenticate and identify the user.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the associated <see cref="User"/> if the ticket is valid, or <c>null</c> if the user cannot be found.</returns>
|
||||
Task<User?> GetUserByCasTicket(string service, string ticket);
|
||||
/// <summary>
|
||||
/// Asynchronously generates a JSON Web Token (JWT) for the specified user and returns the token result.
|
||||
/// </summary>
|
||||
/// <param name="user">The user for whom the JWT is being generated.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the <see cref="TokenResult"/> with the generated token details.</returns>
|
||||
Task<TokenResult> GenerateJwt(User user);
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves all users from the data store.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains a list of all <see cref="User"/> entities.</returns>
|
||||
Task<List<User>> GetAll();
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves a user by their unique identifier, returning <c>null</c> when no user matches the provided id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique <see cref="ObjectId"/> identifier of the user to look up.</param>
|
||||
/// <returns>A task that resolves to the matching <see cref="User"/>, or <c>null</c> if no user is found for the given id.</returns>
|
||||
Task<User?> GetUserById(ObjectId id);
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves a user by their unique username, returning null when no matching user is found.
|
||||
/// </summary>
|
||||
/// <param name="name">The username used to look up the user.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the matching user if found; otherwise, null.</returns>
|
||||
Task<User?> GetUserByUserName(string name);
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves a user matching the specified name, returning <c>null</c> when no matching user is found.
|
||||
/// </summary>
|
||||
/// <param name="name">The name used to look up the user.</param>
|
||||
/// <returns>A task that resolves to the matching <see cref="User"/>, or <c>null</c> if no user with the given name exists.</returns>
|
||||
Task<User?> GetUserByName(string name);
|
||||
/// <summary>
|
||||
/// Creates a user from the provided LDAP user entry.
|
||||
/// </summary>
|
||||
/// <param name="userEntryLdap">The LDAP user entry used to create the user.</param>
|
||||
/// <returns>A task that represents the asynchronous create operation, containing the created <see cref="User"/> or <c>null</c>.</returns>
|
||||
Task<User?> CreateUser(User userEntryLdap);
|
||||
/// <summary>
|
||||
/// Creates a new user based on the provided user data, typically originating from an incoming request.
|
||||
/// Returns the newly created user, or <see langword="null"/> if the user could not be created.
|
||||
/// </summary>
|
||||
/// <param name="user">The user data to use for creating the new user.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The result is the created <see cref="User"/>, or <see langword="null"/> if creation failed.</returns>
|
||||
Task<User?> CreateNewUserByRequest(User user);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new user together with the associated authorities based on the supplied data transfer object.
|
||||
/// </summary>
|
||||
/// <param name="createUserWithAuthDto">The data transfer object containing the information required to create the user and its authorities.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the created <see cref="User"/> instance, or <c>null</c> if no user was created.</returns>
|
||||
Task<User?> CreateNewUserWithAuthorities(CreateUserWithAuthDto createUserWithAuthDto);
|
||||
/// <summary>
|
||||
/// Updates an existing user along with the associated authorities, optionally updating the password.
|
||||
/// </summary>
|
||||
/// <param name="createUserWithAuthDto">The data transfer object containing the user information and authorities to update.</param>
|
||||
/// <param name="updatePass">A flag indicating whether the user's password should be updated as part of the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the updated <see cref="User"/> or <c>null</c> if the user was not found.</returns>
|
||||
Task<User?> UpdateUserWithAuthorities(UpdateUserWithAuthDto createUserWithAuthDto, bool updatePass);
|
||||
/// <summary>
|
||||
/// Updates a user's information based on the provided user data, with an option to include password updates.
|
||||
/// </summary>
|
||||
/// <param name="user">The user entity containing the updated information to be persisted.</param>
|
||||
/// <param name="updatePass">A flag indicating whether the user's password should be updated during this operation.</param>
|
||||
/// <returns>A task that returns the updated <see cref="User"/>, or null if the user could not be found.</returns>
|
||||
Task<User?> UpdateUsersByRequest(User user, bool updatePass);
|
||||
/// <summary>
|
||||
/// Updates the password for the user identified by the specified identifier, verifying the old password before applying the new one.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the user whose password will be updated.</param>
|
||||
/// <param name="oldPassword">The user's current password, used to verify the request.</param>
|
||||
/// <param name="newPassword">The new password to set for the user.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the password was updated successfully; otherwise, <c>false</c>.</returns>
|
||||
Task<bool> UpdateUserPassword(ObjectId id, string oldPassword, string newPassword);
|
||||
/// <summary>
|
||||
/// Asynchronously deletes a user identified by the specified identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the user to delete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing a value indicating whether the user was successfully deleted.</returns>
|
||||
Task<bool> DeleteUser(ObjectId id);
|
||||
/// <summary>
|
||||
/// Retrieves a paginated list of users based on the provided pagination filter configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">The pagination filter that defines paging parameters such as page number and page size.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the paginated response of <see cref="User"/> entries.</returns>
|
||||
Task<PaginationResponse<User>> GetPaginatedUsers(PaginationFilter config);
|
||||
/// <summary>
|
||||
/// Asynchronously creates a new authority based on the provided authorization data.
|
||||
/// </summary>
|
||||
/// <param name="auth">The authorization information used to create the new authority.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the created <see cref="Authorization"/>.</returns>
|
||||
Task<Authorization> CreateNewAuthority(Authorization auth);
|
||||
/// <summary>
|
||||
/// Asynchronously deletes an authority identified by the specified identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the authority to delete.</param>
|
||||
/// <returns>A task that represents the asynchronous delete operation. The task result contains a boolean indicating whether the authority was successfully deleted.</returns>
|
||||
Task<bool> DeleteAuthority(string id);
|
||||
/// <summary>
|
||||
/// Updates the authority information based on the provided authorization data.
|
||||
/// </summary>
|
||||
/// <param name="authorization">The authorization entity containing the authority details to be updated.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the update was successful; otherwise, <c>false</c>.</returns>
|
||||
Task<bool> UpdateAuthority(Authorization authorization);
|
||||
/// <summary>
|
||||
/// Authenticates the user using the provided access token and returns the resulting token information.
|
||||
/// </summary>
|
||||
/// <param name="token">The access token used to perform the login.</param>
|
||||
/// <returns>A task that represents the asynchronous login operation, containing the token result.</returns>
|
||||
Task<TokenResult> LoginWithGivenAccessToken(string token);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace adas_core.Authentication.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a container for login-related information.
|
||||
/// </summary>
|
||||
public class LoginInfo
|
||||
{
|
||||
public string Username { get; set; } = null!;
|
||||
|
||||
@@ -3,6 +3,9 @@ using adas_core.Domain.Models.MongoModels;
|
||||
|
||||
namespace adas_core.Authentication.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a token-related operation, encapsulating the outcome and any associated token data.
|
||||
/// </summary>
|
||||
public class TokenResult
|
||||
{
|
||||
public string AccessToken { get; set; } = null!;
|
||||
@@ -12,6 +15,9 @@ public class TokenResult
|
||||
public User User { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a panel UI component responsible for displaying token-related results.
|
||||
/// </summary>
|
||||
public class TokenResultPanel
|
||||
{
|
||||
public TokenResult TokenResult { get; set; } = null!;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
namespace adas_core.Authentication.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a generic response in the Universal Chess Interface (UCI) protocol, encapsulating a payload of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the response payload.</typeparam>
|
||||
public class UciResponse<T>
|
||||
{
|
||||
protected UciResponse()
|
||||
@@ -21,27 +25,43 @@ public class UciResponse<T>
|
||||
public T? Data { get; protected set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful <see cref="UciResponse{T}"/> wrapping the specified entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to include in the response payload.</param>
|
||||
/// <returns>A <see cref="UciResponse{T}"/> containing the provided entity.</returns>
|
||||
public static UciResponse<T> FromSuccess(T entity)
|
||||
{
|
||||
return new UciResponse<T>(entity);
|
||||
}
|
||||
|
||||
|
||||
public static UciResponse<T> FromError(Exception ex)
|
||||
{
|
||||
var error = ex.GetType().Name;
|
||||
var index = error.LastIndexOf("Exception", StringComparison.Ordinal);
|
||||
if (index > -1) error = error[..index];
|
||||
return FromError(error, ex.Message);
|
||||
}
|
||||
|
||||
public static UciResponse<T> FromError(string error, string? message = null)
|
||||
{
|
||||
return new UciResponse<T>
|
||||
{
|
||||
Success = false,
|
||||
Error = error,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
return new UciResponse<T>(entity);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="UciResponse{T}"/> representing an error state derived from the supplied exception, using the exception's type name (with the "Exception" suffix stripped) as the error code and its message as the error description.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception whose type name and message are used to populate the error response.</param>
|
||||
/// <returns>A <see cref="UciResponse{T}"/> containing the cleaned exception type name and the exception message as the error details.</returns>
|
||||
public static UciResponse<T> FromError(Exception ex)
|
||||
{
|
||||
var error = ex.GetType().Name;
|
||||
var index = error.LastIndexOf("Exception", StringComparison.Ordinal);
|
||||
if (index > -1) error = error[..index];
|
||||
return FromError(error, ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="UciResponse{T}"/> instance representing a failed operation, populating the error information and an optional descriptive message.
|
||||
/// </summary>
|
||||
/// <param name="error">The error code or identifier describing the failure.</param>
|
||||
/// <param name="message">An optional human-readable message providing additional context about the error.</param>
|
||||
/// <returns>A <see cref="UciResponse{T}"/> with <c>Success</c> set to <c>false</c>, the specified <paramref name="error"/>, and the optional <paramref name="message"/>.</returns>
|
||||
public static UciResponse<T> FromError(string error, string? message = null)
|
||||
{
|
||||
return new UciResponse<T>
|
||||
{
|
||||
Success = false,
|
||||
Error = error,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
# adas-core.Authentication — Security & Identity Abstractions
|
||||
|
||||
> The **Authentication Layer** of the ADAS Core platform.
|
||||
> Provides a strategy-pattern abstraction for authentication, JWT token management, user identity resolution, and role-based authorization. This layer defines **who** can access the system, delegating **how** credentials are validated to concrete strategy projects (`LdapLogin`, `LocalLogin`).
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Responsibilities](#responsibilities)
|
||||
3. [Project Structure](#project-structure)
|
||||
4. [Dependencies](#dependencies)
|
||||
5. [Authentication Strategy Pattern](#authentication-strategy-pattern)
|
||||
6. [Core Contracts](#core-contracts)
|
||||
7. [Token Lifecycle](#token-lifecycle)
|
||||
8. [Authorization Attributes](#authorization-attributes)
|
||||
9. [Registration Extensions](#registration-extensions)
|
||||
10. [Extensibility](#extensibility)
|
||||
11. [Design Rules](#design-rules)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`adas-core.Authentication` is the security abstraction layer of the ADAS platform. It establishes the contracts and shared infrastructure for identity management without committing to any single credential store or validation mechanism.
|
||||
|
||||
Key characteristics:
|
||||
|
||||
- **Strategy Pattern** — Authentication is abstracted behind `ILoginService`; concrete strategies (LDAP, Local DB, future OAuth) are implemented in separate satellite projects.
|
||||
- **JWT-Native** — All sessions are represented as signed JWT tokens with refresh-token support.
|
||||
- **Role-Based Access Control** — Authorities (roles/permissions) are modeled in the Domain layer and managed through `IAuthorityService`.
|
||||
- **Framework-Agnostic Contracts** — Authentication contracts depend only on `Domain` and `Application`, never on Infrastructure or Host concerns.
|
||||
- **Extensible by Design** — New authentication strategies (OAuth 2.0, SAML, OIDC) can be added as new projects implementing `ILoginService` without modifying this layer.
|
||||
|
||||
---
|
||||
|
||||
## Responsibilities
|
||||
|
||||
| Concern | What this project does |
|
||||
|---------|----------------------|
|
||||
| **Login Strategy Contract** | Defines `ILoginService` — the universal interface that all concrete login providers must implement. |
|
||||
| **Token Generation** | `ITokenService` issues, serializes, and validates JWT access and refresh tokens. |
|
||||
| **User Identity** | `IUserService` manages user lookup, creation, update, deletion, and association with roles/authorities. |
|
||||
| **Authority Management** | `IAuthorityService` governs role assignment and permission resolution for users. |
|
||||
| **Registration Helper** | `AuthRegistration` provides DI registration helpers consumed by the Host project. |
|
||||
| **Auth Attributes** | Custom authorization attributes (`AuthorizeRolesAttribute`, `PermissionAuthorizeAttribute`, etc.) for declarative security on controllers. |
|
||||
| **Cross-Cutting Security** | Encapsulates token validation parameters, refresh-token rotation logic, and CAS-ticket resolution. |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
adas-core.Authentication/
|
||||
├── Interfaces/ # Core security contracts
|
||||
│ ├── ILoginService.cs # Universal login strategy contract
|
||||
│ ├── ITokenService.cs # JWT generation / serialization
|
||||
│ ├── IUserService.cs # User CRUD + token login / refresh
|
||||
│ └── IAuthorityService.cs # Role / permission management
|
||||
├── Models/ # Authentication DTOs / value objects
|
||||
│ ├── LoginInfo.cs
|
||||
│ ├── Token.cs
|
||||
│ └── UciResponse.cs
|
||||
├── Attributes/ # Declarative authorization attributes
|
||||
│ ├── AuthorizeRolesAttribute.cs
|
||||
│ ├── AuthorizeUserByService.cs
|
||||
│ └── PermissionAuthorizeAttribute.cs
|
||||
├── RegistrationExtensions/
|
||||
│ └── AuthRegistration.cs # DI wiring helpers
|
||||
├── AuthorityService.cs # Concrete authority management
|
||||
├── UserService.cs # Concrete user identity management
|
||||
└── adas-core.Authentication.csproj
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Downstream References
|
||||
|
||||
| Project | Role |
|
||||
|---------|------|
|
||||
| `adas-core.Domain` | `User`, `Authorization`, `UserEnum.LoginMethod`, and other identity entities used by contracts. |
|
||||
| `adas-core.Application` | `PaginationFilter`, `PaginationResponse`, `CreateUserWithAuthDto`, `UpdateUserWithAuthDto` DTOs consumed by `IUserService`. |
|
||||
|
||||
### Upstream References (projects that depend on this)
|
||||
|
||||
| Project | Reason |
|
||||
|---------|--------|
|
||||
| `adas-core` (Host) | Calls `AddAuth()` to register `IUserService`, applies `[Authorize*]` attributes on controllers, and consumes `TokenResult` from login endpoints. |
|
||||
| `adas-core.LdapLogin` | Implements `ILoginService` for LDAP / Active Directory. |
|
||||
| `adas-core.LocalLogin` | Implements `ILoginService` for local database credentials. |
|
||||
| `adas-core.Test` | Mocks `IUserService` and `ITokenService` in unit/integration tests. |
|
||||
|
||||
### NuGet Packages
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| `System.IdentityModel.Tokens.Jwt` | 8.18.0 | JWT token creation, validation, and parsing. |
|
||||
| `Microsoft.IdentityModel.Tokens` | 8.18.0 | Signing key and token validation parameter abstractions. |
|
||||
| `Microsoft.AspNetCore.Http` | 2.3.10 | `HttpContext` consumption in `ILoginService` overloads. |
|
||||
| `Microsoft.AspNetCore.Mvc.NewtonsoftJson` | 8.0.27 | MVC attribute compatibility. |
|
||||
| `AuditLogs` | 1.0.59 | Audit tagging on sensitive identity operations. |
|
||||
|
||||
---
|
||||
|
||||
## Authentication Strategy Pattern
|
||||
|
||||
The layer uses the **Strategy Pattern** to decouple authentication mechanisms from the core identity logic. The Host selects the appropriate strategy at runtime via configuration.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ILoginService {
|
||||
<<interface>>
|
||||
+Method : LoginMethod
|
||||
+AllowPassword : bool
|
||||
+Login(username, password) Task~User~
|
||||
+Login(context) Task~User~
|
||||
+Authenticate(username, password) Task~User~
|
||||
+GetById(id) Task~User?~
|
||||
+GetByEmail(email) Task~User?~
|
||||
+GetByUsername(username) Task~User?~
|
||||
+GetAllUsers() Task~List~User~~
|
||||
}
|
||||
|
||||
class LdapLoginService {
|
||||
+BindToLdap()
|
||||
}
|
||||
|
||||
class LocalLoginService {
|
||||
+HashPassword()
|
||||
}
|
||||
|
||||
class FutureOAuthService {
|
||||
+ExchangeCode()
|
||||
}
|
||||
|
||||
ILoginService <|-- LdapLoginService
|
||||
ILoginService <|-- LocalLoginService
|
||||
ILoginService <|-- FutureOAuthService
|
||||
```
|
||||
|
||||
### Strategy Selection
|
||||
|
||||
The Host determines which strategy to activate via configuration (`Authentication:Scheme`):
|
||||
|
||||
| Scheme | Implementing Project | Description |
|
||||
|--------|-------------------|-------------|
|
||||
| `Ldap` | `adas-core.LdapLogin` | Delegates credential validation to an LDAP / Active Directory server. |
|
||||
| `Local` | `adas-core.LocalLogin` | Validates credentials against locally stored user records with bcrypt hashing. |
|
||||
| *Future* | *adas-core.OAuthLogin* | Could implement OAuth 2.0 / OIDC without touching this layer. |
|
||||
|
||||
> **Critical Rule:** This project defines the strategy contract but contains **zero implementation logic** for any specific credential store.
|
||||
|
||||
---
|
||||
|
||||
## Core Contracts
|
||||
|
||||
### `ILoginService` — Authentication Strategy Contract
|
||||
|
||||
The universal interface implemented by every concrete authentication provider.
|
||||
|
||||
```csharp
|
||||
public interface ILoginService
|
||||
{
|
||||
bool AllowPassword { get; }
|
||||
UserEnum.LoginMethod Method { get; }
|
||||
|
||||
Task<User> Login(string username, string password);
|
||||
Task<User> Login(HttpContext context);
|
||||
Task<User> Authenticate(string username, string password);
|
||||
Task<User?> GetById(ObjectId id);
|
||||
Task<User?> GetByEmail(string email);
|
||||
Task<User?> GetByUsername(string username);
|
||||
Task<List<User>> GetAllUsers();
|
||||
}
|
||||
```
|
||||
|
||||
Key design notes:
|
||||
|
||||
- `AllowPassword` signals whether the strategy supports password-based login (false for certificate-based or token-only strategies).
|
||||
- `Method` discriminates the strategy so the Host can route to the correct implementation.
|
||||
- `Login(HttpContext)` enables context-aware login (e.g., certificate extraction, client assertions).
|
||||
|
||||
### `ITokenService` — JWT Management
|
||||
|
||||
Responsible for issuing, serializing, and validating tokens without knowing user details.
|
||||
|
||||
```csharp
|
||||
public interface ITokenService
|
||||
{
|
||||
int RefreshTokenValidityInDays { get; }
|
||||
SecurityToken? GetToken(string username, List<Claim> claims);
|
||||
string GetRefreshToken();
|
||||
string Serialize(SecurityToken token);
|
||||
}
|
||||
```
|
||||
|
||||
### `IUserService` — Identity Management
|
||||
|
||||
The central identity orchestrator. Combines authentication, token lifecycle, and user administration.
|
||||
|
||||
Key operations:
|
||||
|
||||
| Area | Methods |
|
||||
|------|---------|
|
||||
| **Authentication** | `Login`, `GetUser`, `Authenticate` |
|
||||
| **Token Lifecycle** | `GenerateJwt`, `RefreshToken`, `ValidateToken`, `LoginWithAccessToken`, `LoginWithGivenAccessToken` |
|
||||
| **User CRUD** | `GetAll`, `GetUserById`, `GetUserByUserName`, `GetUserByName`, `CreateUser`, `CreateNewUserByRequest`, `CreateNewUserWithAuthorities`, `UpdateUserWithAuthorities`, `UpdateUsersByRequest`, `UpdateUserPassword`, `DeleteUser`, `GetPaginatedUsers` |
|
||||
| **CAS Integration** | `GetUserByCasTicket` |
|
||||
|
||||
### `IAuthorityService` — Role-Based Access Control
|
||||
|
||||
Manages the link between users and their permissions.
|
||||
|
||||
```csharp
|
||||
public interface IAuthorityService
|
||||
{
|
||||
Task<List<Authorization>> GetUserAuthorities(ObjectId userId);
|
||||
Task<List<Authorization>> GetAllAuthorities();
|
||||
void CreateNew(string roleName, ObjectId userId);
|
||||
Task InsertOne(Authorization authorization);
|
||||
Task updateOne(Authorization authorization);
|
||||
Task<bool> DeleteAuthoritiesForUser(ObjectId id);
|
||||
Task<Authorization?> CreateNewUserAuthority(Authorization authorization);
|
||||
Task<bool> DeleteUserAuthority(ObjectId userAuthorityIdParsed);
|
||||
Task<Authorization?> EditUserAuthority(Authorization authorization);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Token Lifecycle
|
||||
|
||||
The authentication layer manages the complete lifecycle of bearer tokens:
|
||||
|
||||
```
|
||||
[User Credentials] --> [ILoginService.Authenticate] --> [User]
|
||||
|
|
||||
v
|
||||
[IUserService.GenerateJwt]
|
||||
|
|
||||
+---------------------------+---------------------------+
|
||||
| |
|
||||
v v
|
||||
[Access Token] [Refresh Token]
|
||||
(short-lived) (long-lived)
|
||||
| |
|
||||
v v
|
||||
[API Requests] [IUserService.RefreshToken]
|
||||
|
|
||||
v
|
||||
[New Access Token]
|
||||
```
|
||||
|
||||
| Token Type | Expiration | Usage |
|
||||
|------------|-----------|-------|
|
||||
| **Access Token** | Configurable (typically 15–60 min) | Sent in `Authorization` header for every API call. |
|
||||
| **Refresh Token** | `RefreshTokenValidityInDays` (configurable) | Exchanged silently for a new access token without re-entering credentials. |
|
||||
|
||||
---
|
||||
|
||||
## Authorization Attributes
|
||||
|
||||
Declarative security attributes are defined in this layer and applied on controllers in the Host project.
|
||||
|
||||
| Attribute | Purpose | Placement |
|
||||
|-----------|---------|-----------|
|
||||
| `AuthorizeRolesAttribute` | Restricts access to users with one or more specified roles. | Class or method level. |
|
||||
| `AuthorizeUserByService` | Allows service-scoped authorization (e.g., a service account accessing specific units). | Method level. |
|
||||
| `PermissionAuthorizeAttribute` | Fine-grained permission checks beyond role membership. | Method level. |
|
||||
|
||||
> All attributes derive from ASP.NET Core authorization infrastructure but are customized to integrate with the `IAuthorityService` permission model.
|
||||
|
||||
---
|
||||
|
||||
## Registration Extensions
|
||||
|
||||
`AuthRegistration.cs` provides a centralized helper for wiring authentication services into the Host DI container:
|
||||
|
||||
```csharp
|
||||
public static class AuthRegistration
|
||||
{
|
||||
public static void AddAuth(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<IUserService, UserService>();
|
||||
serviceCollection.AddSingleton<Lazy<IUserService>>(provider =>
|
||||
new Lazy<IUserService>(provider.GetRequiredService<IUserService>));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The Host project calls `builder.Services.AddAuth()` during startup, which in turn registers:
|
||||
- `IUserService` singleton
|
||||
- Lazy wrapper for deferred resolution
|
||||
- `ITokenService` and `IAuthorityService` concrete implementations
|
||||
|
||||
Concrete strategy projects (`LdapLogin`, `LocalLogin`) then register their own `ILoginService` implementations, completing the pipeline.
|
||||
|
||||
---
|
||||
|
||||
## Extensibility
|
||||
|
||||
### Adding a New Authentication Strategy
|
||||
|
||||
To introduce a new mechanism (e.g., OAuth 2.0, SAML, certificate-based):
|
||||
|
||||
1. **Create a new project** (e.g., `adas-core.OAuthLogin`).
|
||||
2. **Reference** `adas-core.Authentication` and `adas-core.Domain`.
|
||||
3. **Implement `ILoginService`** with the new credential validation logic.
|
||||
4. **Register** the implementation in the Host project's `Program.cs` alongside `AddAuth()`.
|
||||
5. **Configure** the strategy selector in `appsettings.json`.
|
||||
|
||||
No changes to `adas-core.Authentication` are required. The Host simply resolves the correct `ILoginService` implementation based on runtime configuration.
|
||||
|
||||
### Supported Future Strategies
|
||||
|
||||
| Strategy | Feasibility | Notes |
|
||||
|----------|------------|-------|
|
||||
| **OAuth 2.0 / OIDC** | High | Implement `ILoginService` with code exchange flow. |
|
||||
| **SAML 2.0** | Medium | Parse SAML assertions, map to `User` entity. |
|
||||
| **Certificate-Based (mTLS)** | Medium | Read client certificate from `HttpContext`, validate chain. |
|
||||
| **Kerberos / SPNEGO** | Low | Possible but requires Windows domain integration. |
|
||||
|
||||
---
|
||||
|
||||
## Design Rules
|
||||
|
||||
1. **No Credential Logic in Contracts** — This project defines interfaces, models, and attributes. It never implements LDAP binds, password hashes, or database queries.
|
||||
2. **Strategy Isolation** — Each authentication mechanism lives in its own project implementing `ILoginService`. No conditional logic selecting between strategies inside `Authentication`.
|
||||
3. **Domain Dependency Only** — References only `adas-core.Domain` and `adas-core.Application`. No reference to Infrastructure, Modules, or Host.
|
||||
4. **Token Immutability** — Once issued, a JWT is self-contained. The token service signs it; validation relies on signature alone, avoiding database lookups per request.
|
||||
5. **Lazy Resolution** — `IUserService` is wrapped in `Lazy<T>` to defer heavy initialization until first usage.
|
||||
6. **Audit Awareness** — Every mutating identity operation (create user, change password, update authority) must emit an audit event before returning.
|
||||
7. **No Hardcoded Secrets** — Token signing keys, LDAP server addresses, and refresh-token storage paths come exclusively from configuration; never from source code.
|
||||
8. **RBAC Extensibility** — Authorities are stored as domain entities. Custom roles can be created dynamically without redeploying the authentication layer.
|
||||
9. **Attribute-Driven Security** — Controllers declare authorization requirements via attributes; the Host enforces them through policy evaluation. The Authentication layer provides the building blocks, not enforcement logic.
|
||||
10. **Consistent Error Semantics** — All authentication failures throw or return standard domain exceptions (`UnauthorizedException`, `TokenException`) defined in `adas-core.Application.Exceptions`.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Back to <a href="../README.md">adas-core Root README</a>
|
||||
</p>
|
||||
@@ -3,12 +3,22 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace adas_core.Authentication.RegistrationExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides static functionality related to user authentication registration processes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This static class serves as a container for registration-related authentication operations.
|
||||
/// </remarks>
|
||||
public static class AuthRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers authentication-related services in the dependency injection container, including a singleton <see cref="IUserService"/> and a lazy wrapper for deferred resolution.
|
||||
/// </summary>
|
||||
/// <param name="serviceCollection">The service collection to which the authentication services are added.</param>
|
||||
public static void AddAuth(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<IUserService, UserService>();
|
||||
serviceCollection.AddSingleton<Lazy<IUserService>>(provider =>
|
||||
new Lazy<IUserService>(provider.GetRequiredService<IUserService>));
|
||||
}
|
||||
{
|
||||
serviceCollection.AddSingleton<IUserService, UserService>();
|
||||
serviceCollection.AddSingleton<Lazy<IUserService>>(provider =>
|
||||
new Lazy<IUserService>(provider.GetRequiredService<IUserService>));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user