1037 lines
36 KiB
Markdown
1037 lines
36 KiB
Markdown
# Security Patterns and Implementation - Centron Enterprise Application
|
|
|
|
## Overview
|
|
|
|
This document provides comprehensive coverage of security patterns and implementations within the Centron .NET 8 enterprise application. Security patterns encompass authentication, authorization, data protection, audit logging, secure communication, and security boundary enforcement that protect the application and its data from threats.
|
|
|
|
## Security Architecture
|
|
|
|
### **Multi-Layered Security Approach**
|
|
|
|
The Centron application implements a defense-in-depth security strategy with multiple security layers:
|
|
|
|
1. **Network Security**: Secure communication protocols and network segmentation
|
|
2. **Application Security**: Authentication, authorization, and input validation
|
|
3. **Data Security**: Encryption, data protection, and secure storage
|
|
4. **Audit Security**: Comprehensive logging and monitoring
|
|
5. **Operational Security**: Secure deployment and configuration management
|
|
|
|
## Authentication Patterns
|
|
|
|
### **1. Multi-Authentication Strategy Pattern**
|
|
|
|
The application supports multiple authentication mechanisms through a unified interface:
|
|
|
|
```csharp
|
|
public interface IAuthenticator
|
|
{
|
|
Task<Result<AuthenticationResult>> AuthenticateAsync(AuthenticationRequest request);
|
|
Task<Result> ValidateTokenAsync(string token);
|
|
Task<Result> RefreshTokenAsync(string refreshToken);
|
|
bool SupportsAuthenticationType(AuthenticationType type);
|
|
}
|
|
|
|
public class AuthenticatorFactory
|
|
{
|
|
private readonly Dictionary<AuthenticationType, IAuthenticator> _authenticators = new();
|
|
|
|
public AuthenticatorFactory()
|
|
{
|
|
_authenticators[AuthenticationType.Basic] = new BasicAuthenticator();
|
|
_authenticators[AuthenticationType.ActiveDirectory] = new ActiveDirectoryAuthenticator();
|
|
_authenticators[AuthenticationType.OpenIdConnect] = new OpenIdConnectAuthenticator();
|
|
_authenticators[AuthenticationType.WebAccount] = new WebAccountAuthenticator();
|
|
}
|
|
|
|
public IAuthenticator GetAuthenticator(AuthenticationType type)
|
|
{
|
|
return _authenticators.ContainsKey(type)
|
|
? _authenticators[type]
|
|
: throw new NotSupportedException($"Authentication type {type} is not supported");
|
|
}
|
|
}
|
|
|
|
// Basic Authentication Implementation
|
|
public class BasicAuthenticator : IAuthenticator
|
|
{
|
|
private readonly IUserRepository _userRepository;
|
|
private readonly IPasswordHashingService _passwordService;
|
|
|
|
public async Task<Result<AuthenticationResult>> AuthenticateAsync(AuthenticationRequest request)
|
|
{
|
|
try
|
|
{
|
|
Guard.NotNullOrEmpty(request.Username, nameof(request.Username));
|
|
Guard.NotNullOrEmpty(request.Password, nameof(request.Password));
|
|
|
|
var userResult = await _userRepository.GetUserByUsernameAsync(request.Username);
|
|
if (userResult.Status != ResultStatus.Success || userResult.Data == null)
|
|
return Result<AuthenticationResult>.AsError("Invalid username or password");
|
|
|
|
var user = userResult.Data;
|
|
|
|
// Check if account is locked
|
|
if (user.IsLocked)
|
|
return Result<AuthenticationResult>.AsError("Account is locked. Please contact administrator.");
|
|
|
|
// Verify password
|
|
var passwordValid = _passwordService.VerifyPassword(request.Password, user.PasswordHash, user.Salt);
|
|
if (!passwordValid)
|
|
{
|
|
await IncrementFailedLoginAttempts(user);
|
|
return Result<AuthenticationResult>.AsError("Invalid username or password");
|
|
}
|
|
|
|
// Reset failed login attempts on successful login
|
|
await ResetFailedLoginAttempts(user);
|
|
|
|
// Generate authentication token
|
|
var token = GenerateAuthenticationToken(user);
|
|
|
|
return Result<AuthenticationResult>.AsSuccess(new AuthenticationResult
|
|
{
|
|
User = user,
|
|
Token = token,
|
|
ExpiresAt = DateTime.UtcNow.AddHours(8),
|
|
AuthenticationType = AuthenticationType.Basic
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<AuthenticationResult>.AsError($"Authentication failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task IncrementFailedLoginAttempts(User user)
|
|
{
|
|
user.FailedLoginAttempts++;
|
|
user.LastFailedLogin = DateTime.UtcNow;
|
|
|
|
// Lock account after 5 failed attempts
|
|
if (user.FailedLoginAttempts >= 5)
|
|
{
|
|
user.IsLocked = true;
|
|
user.LockedAt = DateTime.UtcNow;
|
|
}
|
|
|
|
await _userRepository.UpdateUserAsync(user);
|
|
}
|
|
}
|
|
|
|
// Active Directory Authentication
|
|
public class ActiveDirectoryAuthenticator : IAuthenticator
|
|
{
|
|
private readonly IActiveDirectoryService _adService;
|
|
private readonly IUserMappingService _userMappingService;
|
|
|
|
public async Task<Result<AuthenticationResult>> AuthenticateAsync(AuthenticationRequest request)
|
|
{
|
|
try
|
|
{
|
|
// Authenticate against Active Directory
|
|
var adAuthResult = await _adService.AuthenticateUserAsync(request.Username, request.Password);
|
|
if (!adAuthResult.IsSuccess)
|
|
return Result<AuthenticationResult>.AsError("Active Directory authentication failed");
|
|
|
|
// Map AD user to local user
|
|
var userMappingResult = await _userMappingService.GetOrCreateUserFromAdAsync(adAuthResult.User);
|
|
if (userMappingResult.Status != ResultStatus.Success)
|
|
return Result<AuthenticationResult>.FromResult(userMappingResult);
|
|
|
|
var localUser = userMappingResult.Data;
|
|
|
|
// Check user status
|
|
if (!localUser.IsActive)
|
|
return Result<AuthenticationResult>.AsError("User account is disabled");
|
|
|
|
// Generate token
|
|
var token = GenerateAuthenticationToken(localUser);
|
|
|
|
return Result<AuthenticationResult>.AsSuccess(new AuthenticationResult
|
|
{
|
|
User = localUser,
|
|
Token = token,
|
|
ExpiresAt = DateTime.UtcNow.AddHours(8),
|
|
AuthenticationType = AuthenticationType.ActiveDirectory,
|
|
AdditionalClaims = ExtractAdClaims(adAuthResult.User)
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<AuthenticationResult>.AsError($"AD authentication failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// OpenID Connect Authentication
|
|
public class OpenIdConnectAuthenticator : IAuthenticator
|
|
{
|
|
private readonly IOpenIdConnectService _oidcService;
|
|
|
|
public async Task<Result<AuthenticationResult>> AuthenticateAsync(AuthenticationRequest request)
|
|
{
|
|
try
|
|
{
|
|
// Validate OIDC token
|
|
var tokenValidationResult = await _oidcService.ValidateTokenAsync(request.Token);
|
|
if (!tokenValidationResult.IsValid)
|
|
return Result<AuthenticationResult>.AsError("Invalid OpenID Connect token");
|
|
|
|
// Extract user information from token claims
|
|
var userInfo = ExtractUserInfoFromClaims(tokenValidationResult.Claims);
|
|
|
|
// Get or create local user account
|
|
var localUserResult = await GetOrCreateLocalUser(userInfo);
|
|
if (localUserResult.Status != ResultStatus.Success)
|
|
return Result<AuthenticationResult>.FromResult(localUserResult);
|
|
|
|
return Result<AuthenticationResult>.AsSuccess(new AuthenticationResult
|
|
{
|
|
User = localUserResult.Data,
|
|
Token = request.Token,
|
|
ExpiresAt = tokenValidationResult.ExpiresAt,
|
|
AuthenticationType = AuthenticationType.OpenIdConnect
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<AuthenticationResult>.AsError($"OIDC authentication failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Token-Based Authentication Pattern**
|
|
|
|
```csharp
|
|
public class TokenAuthenticationService
|
|
{
|
|
private readonly ITokenGenerator _tokenGenerator;
|
|
private readonly ITokenValidator _tokenValidator;
|
|
private readonly IUserRepository _userRepository;
|
|
|
|
public async Task<Result<string>> GenerateTokenAsync(User user, TimeSpan? expiry = null)
|
|
{
|
|
try
|
|
{
|
|
var expiresAt = DateTime.UtcNow.Add(expiry ?? TimeSpan.FromHours(8));
|
|
|
|
var tokenClaims = new TokenClaims
|
|
{
|
|
UserId = user.I3D,
|
|
Username = user.Username,
|
|
Email = user.Email,
|
|
Roles = user.UserRoles.Select(ur => ur.Role.Name).ToList(),
|
|
ExpiresAt = expiresAt,
|
|
IssuedAt = DateTime.UtcNow
|
|
};
|
|
|
|
var token = _tokenGenerator.GenerateToken(tokenClaims);
|
|
|
|
// Store token in database for revocation capability
|
|
await StoreTokenAsync(token, user.I3D, expiresAt);
|
|
|
|
return Result<string>.AsSuccess(token);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<string>.AsError($"Token generation failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<Result<TokenValidationResult>> ValidateTokenAsync(string token)
|
|
{
|
|
try
|
|
{
|
|
// Check if token is revoked
|
|
var isRevokedResult = await IsTokenRevokedAsync(token);
|
|
if (isRevokedResult.Status == ResultStatus.Success && isRevokedResult.Data)
|
|
return Result<TokenValidationResult>.AsError("Token has been revoked");
|
|
|
|
// Validate token signature and expiry
|
|
var validationResult = _tokenValidator.ValidateToken(token);
|
|
if (!validationResult.IsValid)
|
|
return Result<TokenValidationResult>.AsError(validationResult.Error);
|
|
|
|
// Verify user still exists and is active
|
|
var userResult = await _userRepository.GetUserByIdAsync(validationResult.Claims.UserId);
|
|
if (userResult.Status != ResultStatus.Success || !userResult.Data.IsActive)
|
|
return Result<TokenValidationResult>.AsError("User account is no longer active");
|
|
|
|
return Result<TokenValidationResult>.AsSuccess(new TokenValidationResult
|
|
{
|
|
IsValid = true,
|
|
Claims = validationResult.Claims,
|
|
User = userResult.Data
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<TokenValidationResult>.AsError($"Token validation failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<Result> RevokeTokenAsync(string token)
|
|
{
|
|
return await RevokeTokenInDatabaseAsync(token);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Authorization Patterns
|
|
|
|
### **1. Role-Based Access Control (RBAC) Pattern**
|
|
|
|
```csharp
|
|
public class AuthorizationService
|
|
{
|
|
private readonly IUserRightsRepository _userRightsRepository;
|
|
private readonly IRoleRepository _roleRepository;
|
|
|
|
public async Task<Result<bool>> HasRightAsync(int userId, int rightId)
|
|
{
|
|
try
|
|
{
|
|
var userRightsResult = await _userRightsRepository.GetUserRightsAsync(userId);
|
|
if (userRightsResult.Status != ResultStatus.Success)
|
|
return Result<bool>.FromResult(userRightsResult);
|
|
|
|
var hasRight = userRightsResult.Data.Any(ur => ur.RightI3D == rightId && ur.HasRight);
|
|
|
|
return Result<bool>.AsSuccess(hasRight);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<bool>.AsError($"Authorization check failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<Result<bool>> HasRoleAsync(int userId, string roleName)
|
|
{
|
|
try
|
|
{
|
|
var userRolesResult = await _roleRepository.GetUserRolesAsync(userId);
|
|
if (userRolesResult.Status != ResultStatus.Success)
|
|
return Result<bool>.FromResult(userRolesResult);
|
|
|
|
var hasRole = userRolesResult.Data.Any(r => r.Name.Equals(roleName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
return Result<bool>.AsSuccess(hasRole);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<bool>.AsError($"Role check failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<Result<List<int>>> GetUserRightsAsync(int userId)
|
|
{
|
|
try
|
|
{
|
|
var userRightsResult = await _userRightsRepository.GetUserRightsAsync(userId);
|
|
if (userRightsResult.Status != ResultStatus.Success)
|
|
return Result<List<int>>.FromResult(userRightsResult);
|
|
|
|
var rightIds = userRightsResult.Data
|
|
.Where(ur => ur.HasRight)
|
|
.Select(ur => ur.RightI3D)
|
|
.ToList();
|
|
|
|
return Result<List<int>>.AsSuccess(rightIds);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<List<int>>.AsError($"Failed to get user rights: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Authorization attribute for web service endpoints
|
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
|
public class AuthenticateAttribute : Attribute
|
|
{
|
|
public int[] RequiredRights { get; set; }
|
|
public string[] RequiredRoles { get; set; }
|
|
public bool AllowAnonymous { get; set; } = false;
|
|
|
|
public AuthenticateAttribute(params int[] requiredRights)
|
|
{
|
|
RequiredRights = requiredRights ?? new int[0];
|
|
}
|
|
}
|
|
|
|
// Authorization enforcement interceptor
|
|
public class AuthorizationInterceptor
|
|
{
|
|
private readonly AuthorizationService _authorizationService;
|
|
|
|
public async Task<Result> AuthorizeAsync(MethodInvocationContext context)
|
|
{
|
|
var authAttribute = context.Method.GetCustomAttribute<AuthenticateAttribute>();
|
|
if (authAttribute == null || authAttribute.AllowAnonymous)
|
|
return Result.AsSuccess();
|
|
|
|
var currentUser = context.GetCurrentUser();
|
|
if (currentUser == null)
|
|
return Result.AsError("Authentication required");
|
|
|
|
// Check required rights
|
|
if (authAttribute.RequiredRights != null && authAttribute.RequiredRights.Any())
|
|
{
|
|
foreach (var requiredRight in authAttribute.RequiredRights)
|
|
{
|
|
var hasRightResult = await _authorizationService.HasRightAsync(currentUser.I3D, requiredRight);
|
|
if (hasRightResult.Status != ResultStatus.Success || !hasRightResult.Data)
|
|
return Result.AsError($"Access denied. Required right: {requiredRight}");
|
|
}
|
|
}
|
|
|
|
// Check required roles
|
|
if (authAttribute.RequiredRoles != null && authAttribute.RequiredRoles.Any())
|
|
{
|
|
foreach (var requiredRole in authAttribute.RequiredRoles)
|
|
{
|
|
var hasRoleResult = await _authorizationService.HasRoleAsync(currentUser.I3D, requiredRole);
|
|
if (hasRoleResult.Status != ResultStatus.Success || !hasRoleResult.Data)
|
|
return Result.AsError($"Access denied. Required role: {requiredRole}");
|
|
}
|
|
}
|
|
|
|
return Result.AsSuccess();
|
|
}
|
|
}
|
|
|
|
// Usage in web service methods
|
|
public class CentronRestService : ICentronRestService
|
|
{
|
|
[Authenticate(UserRightsConst.Sales.Customer.SHOW_CUSTOMER)]
|
|
public async Task<Response<AccountDTO>> GetAccount(Request<AccountFilter> request)
|
|
{
|
|
// Method implementation - authorization is enforced by interceptor
|
|
var result = await _accountsLogic.GetAccountAsync(request.Data.Filter, request.Data.MixMode);
|
|
return Response<AccountDTO>.FromResult(result);
|
|
}
|
|
|
|
[Authenticate(UserRightsConst.Administration.Users.MANAGE_USERS)]
|
|
public async Task<Response<List<UserDTO>>> GetUsers(Request<UserFilter> request)
|
|
{
|
|
// Only users with MANAGE_USERS right can access this method
|
|
var result = await _userLogic.GetUsersAsync(request.Data);
|
|
return Response<List<UserDTO>>.FromResult(result);
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Feature-Based Authorization Pattern**
|
|
|
|
```csharp
|
|
public class FeatureAuthorizationService
|
|
{
|
|
private readonly IFeatureRepository _featureRepository;
|
|
|
|
public async Task<Result<bool>> IsFeatureEnabledForUserAsync(int userId, string featureName)
|
|
{
|
|
try
|
|
{
|
|
var userFeaturesResult = await _featureRepository.GetUserFeaturesAsync(userId);
|
|
if (userFeaturesResult.Status != ResultStatus.Success)
|
|
return Result<bool>.FromResult(userFeaturesResult);
|
|
|
|
var isEnabled = userFeaturesResult.Data.Any(f =>
|
|
f.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase) && f.IsEnabled);
|
|
|
|
return Result<bool>.AsSuccess(isEnabled);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<bool>.AsError($"Feature authorization check failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Feature gate attribute
|
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
|
public class RequireFeatureAttribute : Attribute
|
|
{
|
|
public string FeatureName { get; }
|
|
|
|
public RequireFeatureAttribute(string featureName)
|
|
{
|
|
FeatureName = featureName ?? throw new ArgumentNullException(nameof(featureName));
|
|
}
|
|
}
|
|
|
|
// Usage in UI modules
|
|
[RequireFeature(FeatureConst.ADVANCED_REPORTING)]
|
|
public class AdvancedReportModule : ICentronAppModuleController
|
|
{
|
|
public bool IsAvailable(User user)
|
|
{
|
|
// Feature authorization is checked by framework
|
|
return true;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Data Protection Patterns
|
|
|
|
### **1. Encryption and Data Security Pattern**
|
|
|
|
```csharp
|
|
public interface IDataProtectionService
|
|
{
|
|
Result<string> EncryptSensitiveData(string plainText);
|
|
Result<string> DecryptSensitiveData(string encryptedText);
|
|
Result<string> HashPassword(string password, out string salt);
|
|
Result<bool> VerifyPassword(string password, string hash, string salt);
|
|
}
|
|
|
|
public class DataProtectionService : IDataProtectionService
|
|
{
|
|
private readonly IDataProtector _dataProtector;
|
|
private readonly IPasswordHasher _passwordHasher;
|
|
|
|
public DataProtectionService(IDataProtectionProvider dataProtectionProvider)
|
|
{
|
|
_dataProtector = dataProtectionProvider.CreateProtector("Centron.SensitiveData.v1");
|
|
_passwordHasher = new PasswordHasher();
|
|
}
|
|
|
|
public Result<string> EncryptSensitiveData(string plainText)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(plainText))
|
|
return Result<string>.AsSuccess(plainText);
|
|
|
|
var encrypted = _dataProtector.Protect(plainText);
|
|
return Result<string>.AsSuccess(encrypted);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<string>.AsError($"Encryption failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public Result<string> DecryptSensitiveData(string encryptedText)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(encryptedText))
|
|
return Result<string>.AsSuccess(encryptedText);
|
|
|
|
var decrypted = _dataProtector.Unprotect(encryptedText);
|
|
return Result<string>.AsSuccess(decrypted);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<string>.AsError($"Decryption failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public Result<string> HashPassword(string password, out string salt)
|
|
{
|
|
try
|
|
{
|
|
salt = GenerateSalt();
|
|
var hash = _passwordHasher.HashPassword(password + salt);
|
|
return Result<string>.AsSuccess(hash);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
salt = null;
|
|
return Result<string>.AsError($"Password hashing failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public Result<bool> VerifyPassword(string password, string hash, string salt)
|
|
{
|
|
try
|
|
{
|
|
var isValid = _passwordHasher.VerifyPassword(password + salt, hash);
|
|
return Result<bool>.AsSuccess(isValid);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<bool>.AsError($"Password verification failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private string GenerateSalt()
|
|
{
|
|
using (var rng = RandomNumberGenerator.Create())
|
|
{
|
|
var saltBytes = new byte[32];
|
|
rng.GetBytes(saltBytes);
|
|
return Convert.ToBase64String(saltBytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Entity-level data protection
|
|
public class CustomerEntity
|
|
{
|
|
public int I3D { get; set; }
|
|
public string CompanyName { get; set; }
|
|
|
|
[SensitiveData]
|
|
public string Email { get; set; }
|
|
|
|
[SensitiveData]
|
|
public string PhoneNumber { get; set; }
|
|
|
|
[SensitiveData]
|
|
public string BankAccountNumber { get; set; }
|
|
|
|
// Navigation properties
|
|
public virtual List<CustomerAddress> Addresses { get; set; }
|
|
}
|
|
|
|
// Data protection interceptor
|
|
public class SensitiveDataInterceptor
|
|
{
|
|
private readonly IDataProtectionService _dataProtection;
|
|
|
|
public void OnSaving(object entity)
|
|
{
|
|
var properties = entity.GetType().GetProperties()
|
|
.Where(p => p.GetCustomAttribute<SensitiveDataAttribute>() != null);
|
|
|
|
foreach (var property in properties)
|
|
{
|
|
var value = property.GetValue(entity) as string;
|
|
if (!string.IsNullOrEmpty(value))
|
|
{
|
|
var encryptedResult = _dataProtection.EncryptSensitiveData(value);
|
|
if (encryptedResult.Status == ResultStatus.Success)
|
|
{
|
|
property.SetValue(entity, encryptedResult.Data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnLoading(object entity)
|
|
{
|
|
var properties = entity.GetType().GetProperties()
|
|
.Where(p => p.GetCustomAttribute<SensitiveDataAttribute>() != null);
|
|
|
|
foreach (var property in properties)
|
|
{
|
|
var value = property.GetValue(entity) as string;
|
|
if (!string.IsNullOrEmpty(value))
|
|
{
|
|
var decryptedResult = _dataProtection.DecryptSensitiveData(value);
|
|
if (decryptedResult.Status == ResultStatus.Success)
|
|
{
|
|
property.SetValue(entity, decryptedResult.Data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Audit Logging and Compliance Patterns
|
|
|
|
### **1. Comprehensive Audit Trail Pattern**
|
|
|
|
```csharp
|
|
public class AuditService
|
|
{
|
|
private readonly IAuditRepository _auditRepository;
|
|
private readonly ICurrentUserService _currentUserService;
|
|
|
|
public async Task<Result> LogActionAsync(AuditLogEntry auditEntry)
|
|
{
|
|
try
|
|
{
|
|
auditEntry.UserId = _currentUserService.GetCurrentUserId();
|
|
auditEntry.Timestamp = DateTime.UtcNow;
|
|
auditEntry.IpAddress = _currentUserService.GetCurrentUserIpAddress();
|
|
auditEntry.UserAgent = _currentUserService.GetCurrentUserAgent();
|
|
|
|
var result = await _auditRepository.SaveAuditEntryAsync(auditEntry);
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Audit logging should never fail the main operation
|
|
// Log error but return success
|
|
LogAuditError(ex);
|
|
return Result.AsSuccess();
|
|
}
|
|
}
|
|
|
|
public async Task<Result> LogDataAccessAsync(string tableName, int recordId, DataAccessType accessType, object oldValues = null, object newValues = null)
|
|
{
|
|
var auditEntry = new AuditLogEntry
|
|
{
|
|
ActionType = "DATA_ACCESS",
|
|
TableName = tableName,
|
|
RecordId = recordId,
|
|
AccessType = accessType.ToString(),
|
|
OldValues = oldValues != null ? JsonSerializer.Serialize(oldValues) : null,
|
|
NewValues = newValues != null ? JsonSerializer.Serialize(newValues) : null
|
|
};
|
|
|
|
return await LogActionAsync(auditEntry);
|
|
}
|
|
|
|
public async Task<Result> LogBusinessActionAsync(string actionName, string entityType, int? entityId, Dictionary<string, object> parameters = null)
|
|
{
|
|
var auditEntry = new AuditLogEntry
|
|
{
|
|
ActionType = "BUSINESS_ACTION",
|
|
ActionName = actionName,
|
|
EntityType = entityType,
|
|
EntityId = entityId,
|
|
Parameters = parameters != null ? JsonSerializer.Serialize(parameters) : null
|
|
};
|
|
|
|
return await LogActionAsync(auditEntry);
|
|
}
|
|
|
|
public async Task<Result> LogSecurityEventAsync(SecurityEventType eventType, string description, Dictionary<string, object> details = null)
|
|
{
|
|
var auditEntry = new AuditLogEntry
|
|
{
|
|
ActionType = "SECURITY_EVENT",
|
|
SecurityEventType = eventType.ToString(),
|
|
Description = description,
|
|
Details = details != null ? JsonSerializer.Serialize(details) : null,
|
|
Severity = GetSeverityForSecurityEvent(eventType)
|
|
};
|
|
|
|
return await LogActionAsync(auditEntry);
|
|
}
|
|
}
|
|
|
|
// Audit interceptor for automatic logging
|
|
public class AuditInterceptor
|
|
{
|
|
private readonly AuditService _auditService;
|
|
|
|
public async Task LogMethodCallAsync(MethodInvocationContext context)
|
|
{
|
|
var auditAttribute = context.Method.GetCustomAttribute<AuditableAttribute>();
|
|
if (auditAttribute == null) return;
|
|
|
|
var parameters = context.Parameters?.ToDictionary(p => p.Key, p => p.Value);
|
|
|
|
await _auditService.LogBusinessActionAsync(
|
|
context.Method.Name,
|
|
context.TargetType.Name,
|
|
ExtractEntityId(context.Parameters),
|
|
parameters);
|
|
}
|
|
|
|
public async Task LogDataChangeAsync<T>(T entity, DataChangeType changeType, T oldEntity = default) where T : class
|
|
{
|
|
var entityType = typeof(T).Name;
|
|
var entityId = GetEntityId(entity);
|
|
|
|
await _auditService.LogDataAccessAsync(
|
|
entityType,
|
|
entityId,
|
|
ConvertChangeTypeToAccessType(changeType),
|
|
oldEntity,
|
|
entity);
|
|
}
|
|
}
|
|
|
|
// Usage in business logic
|
|
public class AccountBL : BaseBL
|
|
{
|
|
private readonly AuditService _auditService;
|
|
|
|
[Auditable]
|
|
public async Task<Result<Account>> SaveAccountAsync(Account account)
|
|
{
|
|
var oldAccount = account.I3D > 0 ? await GetAccountByIdAsync(account.I3D) : null;
|
|
|
|
// Save the account
|
|
var saveResult = await _accountRepository.SaveAsync(account);
|
|
if (saveResult.Status != ResultStatus.Success)
|
|
return saveResult;
|
|
|
|
// Log the change
|
|
await _auditService.LogDataAccessAsync(
|
|
"Account",
|
|
account.I3D,
|
|
oldAccount == null ? DataAccessType.Create : DataAccessType.Update,
|
|
oldAccount,
|
|
account);
|
|
|
|
return saveResult;
|
|
}
|
|
|
|
[Auditable]
|
|
public async Task<Result> DeleteAccountAsync(int accountId)
|
|
{
|
|
var account = await GetAccountByIdAsync(accountId);
|
|
|
|
var result = await _accountRepository.DeleteAsync(accountId);
|
|
if (result.Status == ResultStatus.Success)
|
|
{
|
|
await _auditService.LogDataAccessAsync("Account", accountId, DataAccessType.Delete, account, null);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. GDPR Compliance Pattern**
|
|
|
|
```csharp
|
|
public class DataProtectionComplianceService
|
|
{
|
|
private readonly IAuditService _auditService;
|
|
private readonly IPersonalDataService _personalDataService;
|
|
|
|
public async Task<Result> ProcessDataDeletionRequestAsync(int customerId, string requestReason)
|
|
{
|
|
try
|
|
{
|
|
// Log the data deletion request
|
|
await _auditService.LogBusinessActionAsync(
|
|
"GDPR_DATA_DELETION_REQUEST",
|
|
"Customer",
|
|
customerId,
|
|
new Dictionary<string, object> { { "Reason", requestReason } });
|
|
|
|
// Identify all personal data related to customer
|
|
var personalDataResult = await _personalDataService.GetCustomerPersonalDataAsync(customerId);
|
|
if (personalDataResult.Status != ResultStatus.Success)
|
|
return Result.FromResult(personalDataResult);
|
|
|
|
// Anonymize or delete personal data
|
|
foreach (var dataLocation in personalDataResult.Data)
|
|
{
|
|
var anonymizationResult = await AnonymizePersonalDataAsync(dataLocation);
|
|
if (anonymizationResult.Status != ResultStatus.Success)
|
|
{
|
|
await _auditService.LogBusinessActionAsync(
|
|
"GDPR_DATA_DELETION_FAILED",
|
|
dataLocation.EntityType,
|
|
dataLocation.EntityId,
|
|
new Dictionary<string, object> { { "Error", anonymizationResult.Error } });
|
|
return anonymizationResult;
|
|
}
|
|
}
|
|
|
|
// Log successful completion
|
|
await _auditService.LogBusinessActionAsync(
|
|
"GDPR_DATA_DELETION_COMPLETED",
|
|
"Customer",
|
|
customerId,
|
|
new Dictionary<string, object> { { "DeletedDataCount", personalDataResult.Data.Count } });
|
|
|
|
return Result.AsSuccess();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _auditService.LogSecurityEventAsync(
|
|
SecurityEventType.GdprComplianceFailure,
|
|
$"GDPR data deletion failed for customer {customerId}",
|
|
new Dictionary<string, object> { { "Exception", ex.Message } });
|
|
|
|
return Result.AsError($"Data deletion request processing failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<Result<PersonalDataExport>> ProcessDataExportRequestAsync(int customerId)
|
|
{
|
|
try
|
|
{
|
|
await _auditService.LogBusinessActionAsync(
|
|
"GDPR_DATA_EXPORT_REQUEST",
|
|
"Customer",
|
|
customerId);
|
|
|
|
var personalDataResult = await _personalDataService.GetCustomerPersonalDataAsync(customerId);
|
|
if (personalDataResult.Status != ResultStatus.Success)
|
|
return Result<PersonalDataExport>.FromResult(personalDataResult);
|
|
|
|
var exportData = new PersonalDataExport
|
|
{
|
|
CustomerId = customerId,
|
|
ExportDate = DateTime.UtcNow,
|
|
Data = personalDataResult.Data.ToDictionary(d => d.DataType, d => d.Value)
|
|
};
|
|
|
|
await _auditService.LogBusinessActionAsync(
|
|
"GDPR_DATA_EXPORT_COMPLETED",
|
|
"Customer",
|
|
customerId,
|
|
new Dictionary<string, object> { { "ExportedDataCount", exportData.Data.Count } });
|
|
|
|
return Result<PersonalDataExport>.AsSuccess(exportData);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<PersonalDataExport>.AsError($"Data export request processing failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Secure Communication Patterns
|
|
|
|
### **1. HTTPS and Certificate Management**
|
|
|
|
```csharp
|
|
public class SecureCommunicationService
|
|
{
|
|
public static HttpClient CreateSecureHttpClient(CertificateValidationMode validationMode = CertificateValidationMode.Default)
|
|
{
|
|
var handler = new HttpClientHandler();
|
|
|
|
switch (validationMode)
|
|
{
|
|
case CertificateValidationMode.Strict:
|
|
handler.ServerCertificateCustomValidationCallback = ValidateCertificateStrict;
|
|
break;
|
|
case CertificateValidationMode.AllowSelfSigned:
|
|
handler.ServerCertificateCustomValidationCallback = ValidateCertificateAllowSelfSigned;
|
|
break;
|
|
case CertificateValidationMode.Development:
|
|
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, errors) => true;
|
|
break;
|
|
}
|
|
|
|
var client = new HttpClient(handler);
|
|
client.DefaultRequestHeaders.Add("User-Agent", "CentronClient/1.0");
|
|
|
|
return client;
|
|
}
|
|
|
|
private static bool ValidateCertificateStrict(HttpRequestMessage request, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors errors)
|
|
{
|
|
// Only allow certificates with no errors
|
|
return errors == SslPolicyErrors.None;
|
|
}
|
|
|
|
private static bool ValidateCertificateAllowSelfSigned(HttpRequestMessage request, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors errors)
|
|
{
|
|
// Allow self-signed certificates but validate other aspects
|
|
if (errors == SslPolicyErrors.None)
|
|
return true;
|
|
|
|
if (errors == SslPolicyErrors.RemoteCertificateChainErrors)
|
|
{
|
|
// Check if the only error is self-signed certificate
|
|
foreach (X509ChainStatus status in chain.ChainStatus)
|
|
{
|
|
if (status.Status != X509ChainStatusFlags.UntrustedRoot)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. API Security Pattern**
|
|
|
|
```csharp
|
|
public class ApiSecurityMiddleware
|
|
{
|
|
private readonly RequestDelegate _next;
|
|
private readonly IApiKeyValidator _apiKeyValidator;
|
|
private readonly IRateLimiter _rateLimiter;
|
|
|
|
public async Task InvokeAsync(HttpContext context)
|
|
{
|
|
// API Key validation
|
|
var apiKeyResult = await ValidateApiKeyAsync(context);
|
|
if (apiKeyResult.Status != ResultStatus.Success)
|
|
{
|
|
context.Response.StatusCode = 401;
|
|
await context.Response.WriteAsync("Unauthorized: Invalid API key");
|
|
return;
|
|
}
|
|
|
|
// Rate limiting
|
|
var rateLimitResult = await _rateLimiter.CheckRateLimitAsync(context);
|
|
if (!rateLimitResult.IsAllowed)
|
|
{
|
|
context.Response.StatusCode = 429;
|
|
context.Response.Headers.Add("Retry-After", rateLimitResult.RetryAfter.ToString());
|
|
await context.Response.WriteAsync("Rate limit exceeded");
|
|
return;
|
|
}
|
|
|
|
// Request size limiting
|
|
if (context.Request.ContentLength > 10_000_000) // 10MB limit
|
|
{
|
|
context.Response.StatusCode = 413;
|
|
await context.Response.WriteAsync("Request too large");
|
|
return;
|
|
}
|
|
|
|
// Add security headers
|
|
AddSecurityHeaders(context);
|
|
|
|
await _next(context);
|
|
}
|
|
|
|
private void AddSecurityHeaders(HttpContext context)
|
|
{
|
|
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
|
|
context.Response.Headers.Add("X-Frame-Options", "DENY");
|
|
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
|
|
context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'");
|
|
}
|
|
}
|
|
```
|
|
|
|
## Security Best Practices and Guidelines
|
|
|
|
### **1. Input Sanitization and Validation**
|
|
- **All user inputs are validated and sanitized** before processing
|
|
- **SQL injection prevention** through parameterized queries and ORM usage
|
|
- **XSS protection** through output encoding and Content Security Policy
|
|
- **Command injection prevention** through input validation and sanitization
|
|
|
|
### **2. Authentication Security**
|
|
- **Multi-factor authentication** support for high-privilege accounts
|
|
- **Account lockout policies** to prevent brute force attacks
|
|
- **Password complexity requirements** with secure hashing algorithms
|
|
- **Session management** with secure tokens and proper expiration
|
|
|
|
### **3. Authorization Security**
|
|
- **Principle of least privilege** - users only get necessary permissions
|
|
- **Role-based access control** with granular permission management
|
|
- **Feature flags** for controlling access to application features
|
|
- **Regular permission audits** and access reviews
|
|
|
|
### **4. Data Protection Security**
|
|
- **Encryption at rest** for sensitive data using industry-standard algorithms
|
|
- **Encryption in transit** using TLS 1.2+ for all communications
|
|
- **Key management** with secure key storage and rotation policies
|
|
- **Data classification** and appropriate protection levels
|
|
|
|
### **5. Monitoring and Incident Response**
|
|
- **Security event logging** with centralized log management
|
|
- **Real-time threat detection** and automated response
|
|
- **Incident response procedures** with defined escalation paths
|
|
- **Regular security assessments** and penetration testing
|
|
|
|
## Conclusion
|
|
|
|
The Centron application implements comprehensive security patterns that provide:
|
|
|
|
- **Defense in Depth**: Multiple security layers protect against various threat vectors
|
|
- **Authentication Flexibility**: Support for multiple authentication mechanisms
|
|
- **Granular Authorization**: Fine-grained permission control at feature and data levels
|
|
- **Data Protection**: Strong encryption and data protection compliance (GDPR)
|
|
- **Audit Trail**: Complete logging of security events and data access
|
|
- **Secure Communication**: Encrypted communication channels and API security
|
|
- **Compliance**: Built-in support for regulatory compliance requirements
|
|
|
|
These security patterns ensure the application meets enterprise security standards while maintaining usability and performance. |