Add Admin roles

This commit is contained in:
2025-08-16 06:06:02 +07:00
parent 7923b38a26
commit 4ff2ccdae3
7 changed files with 332 additions and 3 deletions

View File

@@ -0,0 +1,80 @@
# Admin Feature Documentation
## Overview
The admin feature allows specific users to manage all bots in the system, regardless of ownership. Admin users can start, stop, delete, and modify any bot without owning the associated account.
## How It Works
Admin privileges are granted through environment variables, making it secure and environment-specific. The system checks if a user is an admin by comparing their username against a comma-separated list of admin usernames configured in the environment.
## Configuration
### Environment Variable
Set the `AdminUsers` environment variable with a comma-separated list of usernames:
```bash
AdminUsers=admin1,superuser,john.doe
```
### CapRover Configuration
In your CapRover dashboard:
1. Go to your app's settings
2. Navigate to "Environment Variables"
3. Add a new environment variable:
- Key: `AdminUsers`
- Value: `admin1,superuser,john.doe` (replace with actual admin usernames)
### Local Development
For local development, you can set this in your `appsettings.Development.json`:
```json
{
"AdminUsers": "admin1,superuser,john.doe"
}
```
## Admin Capabilities
Admin users can perform all bot operations without ownership restrictions:
- **Start/Save Bot**: Create and start bots for any account
- **Stop Bot**: Stop any running bot
- **Delete Bot**: Delete any bot
- **Restart Bot**: Restart any bot
- **Open/Close Positions**: Manually open or close positions for any bot
- **Update Configuration**: Modify any bot's configuration
- **View Bot Configuration**: Access any bot's configuration details
## Security Notes
1. **Environment-Based**: Admin users are configured via environment variables, not through the API
2. **No Privilege Escalation**: Regular users cannot grant themselves admin access
3. **Audit Logging**: All admin actions are logged with the admin user's context
4. **Case-Insensitive**: Username matching is case-insensitive for convenience
## Implementation Details
The admin feature is implemented using:
- `IAdminConfigurationService`: Checks if a user is an admin
- Updated `UserOwnsBotAccount` method: Returns true for admin users
- Dependency injection: Service is registered as a singleton
- Configuration reading: Reads from `AdminUsers` environment variable
## Example Usage
1. **Set Admin Users**:
```bash
AdminUsers=alice,bob,charlie
```
2. **Admin Operations**:
- Alice, Bob, or Charlie can now manage any bot in the system
- They can use all existing bot endpoints without ownership restrictions
- All operations are logged with their username for audit purposes
## Troubleshooting
- **Admin not working**: Check if the username exactly matches the configuration (case-insensitive)
- **No admins configured**: Check the `AdminUsers` environment variable is set
- **Multiple environments**: Each environment (dev, staging, prod) should have its own admin configuration

View File

@@ -4,6 +4,7 @@ using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs; using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands; using Managing.Application.ManageBot.Commands;
using Managing.Application.Shared;
using Managing.Common; using Managing.Common;
using Managing.Core; using Managing.Core;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
@@ -40,6 +41,7 @@ public class BotController : BaseController
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IMoneyManagementService _moneyManagementService; private readonly IMoneyManagementService _moneyManagementService;
private readonly IServiceScopeFactory _scopeFactory; private readonly IServiceScopeFactory _scopeFactory;
private readonly IAdminConfigurationService _adminService;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BotController"/> class. /// Initializes a new instance of the <see cref="BotController"/> class.
@@ -56,7 +58,7 @@ public class BotController : BaseController
public BotController(ILogger<BotController> logger, IMediator mediator, IHubContext<BotHub> hubContext, public BotController(ILogger<BotController> logger, IMediator mediator, IHubContext<BotHub> hubContext,
IBacktester backtester, IBotService botService, IUserService userService, IBacktester backtester, IBotService botService, IUserService userService,
IAccountService accountService, IMoneyManagementService moneyManagementService, IAccountService accountService, IMoneyManagementService moneyManagementService,
IServiceScopeFactory scopeFactory) : base(userService) IServiceScopeFactory scopeFactory, IAdminConfigurationService adminService) : base(userService)
{ {
_logger = logger; _logger = logger;
_mediator = mediator; _mediator = mediator;
@@ -66,6 +68,7 @@ public class BotController : BaseController
_accountService = accountService; _accountService = accountService;
_moneyManagementService = moneyManagementService; _moneyManagementService = moneyManagementService;
_scopeFactory = scopeFactory; _scopeFactory = scopeFactory;
_adminService = adminService;
} }
/// <summary> /// <summary>
@@ -73,7 +76,7 @@ public class BotController : BaseController
/// </summary> /// </summary>
/// <param name="identifier">The identifier of the bot to check</param> /// <param name="identifier">The identifier of the bot to check</param>
/// <param name="accountName">Optional account name to check when creating a new bot</param> /// <param name="accountName">Optional account name to check when creating a new bot</param>
/// <returns>True if the user owns the account, False otherwise</returns> /// <returns>True if the user owns the account or is admin, False otherwise</returns>
private async Task<bool> UserOwnsBotAccount(Guid identifier, string accountName = null) private async Task<bool> UserOwnsBotAccount(Guid identifier, string accountName = null)
{ {
try try
@@ -82,6 +85,9 @@ public class BotController : BaseController
if (user == null) if (user == null)
return false; return false;
// Admin users can access all bots
if (_adminService.IsUserAdmin(user.Name))
return true;
if (identifier != default) if (identifier != default)
{ {

View File

@@ -1,5 +1,6 @@
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Shared;
using Managing.Application.Trading.Commands; using Managing.Application.Trading.Commands;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Trades; using Managing.Domain.Trades;
@@ -26,6 +27,8 @@ public class TradingController : BaseController
private readonly IMoneyManagementService _moneyManagementService; private readonly IMoneyManagementService _moneyManagementService;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly ILogger<TradingController> _logger; private readonly ILogger<TradingController> _logger;
private readonly IAdminConfigurationService _adminService;
private readonly IAccountService _accountService;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TradingController"/> class. /// Initializes a new instance of the <see cref="TradingController"/> class.
@@ -35,13 +38,16 @@ public class TradingController : BaseController
/// <param name="closeTradeCommandHandler">Command handler for closing trades.</param> /// <param name="closeTradeCommandHandler">Command handler for closing trades.</param>
/// <param name="tradingService">Service for trading operations.</param> /// <param name="tradingService">Service for trading operations.</param>
/// <param name="mediator">Mediator for handling commands and requests.</param> /// <param name="mediator">Mediator for handling commands and requests.</param>
/// <param name="adminService">Service for checking admin privileges.</param>
/// <param name="accountService">Service for account operations.</param>
public TradingController( public TradingController(
ILogger<TradingController> logger, ILogger<TradingController> logger,
ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler, ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler,
ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler, ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler,
ITradingService tradingService, ITradingService tradingService,
IMediator mediator, IMoneyManagementService moneyManagementService, IMediator mediator, IMoneyManagementService moneyManagementService,
IUserService userService) : base(userService) IUserService userService, IAdminConfigurationService adminService,
IAccountService accountService) : base(userService)
{ {
_logger = logger; _logger = logger;
_openTradeCommandHandler = openTradeCommandHandler; _openTradeCommandHandler = openTradeCommandHandler;
@@ -49,6 +55,8 @@ public class TradingController : BaseController
_tradingService = tradingService; _tradingService = tradingService;
_mediator = mediator; _mediator = mediator;
_moneyManagementService = moneyManagementService; _moneyManagementService = moneyManagementService;
_adminService = adminService;
_accountService = accountService;
} }
@@ -149,6 +157,7 @@ public class TradingController : BaseController
/// <summary> /// <summary>
/// Initializes a Privy wallet address for the user. /// Initializes a Privy wallet address for the user.
/// Only admins can initialize any address, regular users can only initialize their own addresses.
/// </summary> /// </summary>
/// <param name="publicAddress">The public address of the Privy wallet to initialize.</param> /// <param name="publicAddress">The public address of the Privy wallet to initialize.</param>
/// <returns>The initialization response containing success status and transaction hashes.</returns> /// <returns>The initialization response containing success status and transaction hashes.</returns>
@@ -162,6 +171,18 @@ public class TradingController : BaseController
try try
{ {
var user = await GetUser();
if (user == null)
{
return Unauthorized("User not found");
}
// Check if user has permission to initialize this address
if (!await CanUserInitializeAddress(user.Name, publicAddress))
{
return Forbid("You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses.");
}
var result = await _tradingService.InitPrivyWallet(publicAddress); var result = await _tradingService.InitPrivyWallet(publicAddress);
return Ok(result); return Ok(result);
} }
@@ -175,4 +196,42 @@ public class TradingController : BaseController
}); });
} }
} }
/// <summary>
/// Checks if the user can initialize the given public address.
/// Admins can initialize any address, regular users can only initialize their own addresses.
/// </summary>
/// <param name="userName">The username to check</param>
/// <param name="publicAddress">The public address to initialize</param>
/// <returns>True if the user can initialize the address, false otherwise</returns>
private async Task<bool> CanUserInitializeAddress(string userName, string publicAddress)
{
// Admin users can initialize any address
if (_adminService.IsUserAdmin(userName))
{
_logger.LogInformation("Admin user {UserName} initializing address {Address}", userName, publicAddress);
return true;
}
try
{
// Regular users can only initialize their own addresses
// Check if the address belongs to one of the user's accounts
var account = await _accountService.GetAccountByKey(publicAddress, true, false);
if (account?.User?.Name == userName)
{
_logger.LogInformation("User {UserName} initializing their own address {Address}", userName, publicAddress);
return true;
}
_logger.LogWarning("User {UserName} attempted to initialize address {Address} that doesn't belong to them", userName, publicAddress);
return false;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to verify ownership of address {Address} for user {UserName}", publicAddress, userName);
return false;
}
}
} }

View File

@@ -0,0 +1,125 @@
# TradingController Security Enhancement
## Overview
The `InitPrivyWallet` endpoint in `TradingController` has been enhanced with admin role security. This ensures that only authorized users can initialize wallet addresses.
## Security Rules
### For Regular Users
- Can **only** initialize wallet addresses that they own
- The system verifies ownership by checking if the provided public address exists in one of the user's accounts
- If a user tries to initialize an address they don't own, they receive a `403 Forbidden` response
### For Admin Users
- Can initialize **any** wallet address in the system
- Admin status is determined by the `AdminUsers` environment variable
- All admin actions are logged for audit purposes
## Implementation Details
### Authentication Flow
1. **User Authentication**: Endpoint requires valid JWT token
2. **Admin Check**: System checks if user is in the admin list via `IAdminConfigurationService`
3. **Ownership Verification**: For non-admin users, verifies address ownership via `IAccountService.GetAccountByKey()`
4. **Action Logging**: All operations are logged with user context
### Security Validation
```csharp
private async Task<bool> CanUserInitializeAddress(string userName, string publicAddress)
{
// Admin users can initialize any address
if (_adminService.IsUserAdmin(userName))
{
return true;
}
// Regular users can only initialize their own addresses
var account = await _accountService.GetAccountByKey(publicAddress, true, false);
return account?.User?.Name == userName;
}
```
## Error Responses
### 401 Unauthorized
- Missing or invalid JWT token
- User not found in system
### 403 Forbidden
- Non-admin user trying to initialize address they don't own
- Message: "You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses."
### 400 Bad Request
- Empty or null public address provided
### 500 Internal Server Error
- System error during wallet initialization
- Database connectivity issues
- External service failures
## Admin Configuration
Set admin users via environment variable:
```bash
AdminUsers=admin1,admin2,superuser
```
## Audit Logging
All operations are logged with appropriate context:
**Admin Operations:**
```
Admin user {UserName} initializing address {Address}
```
**User Operations:**
```
User {UserName} initializing their own address {Address}
```
**Security Violations:**
```
User {UserName} attempted to initialize address {Address} that doesn't belong to them
```
## Usage Examples
### Regular User - Own Address
```bash
POST /Trading/InitPrivyWallet
Authorization: Bearer {user-jwt-token}
Content-Type: application/json
"0x1234567890123456789012345678901234567890"
```
**Result**: ✅ Success (if address belongs to user)
### Regular User - Other's Address
```bash
POST /Trading/InitPrivyWallet
Authorization: Bearer {user-jwt-token}
Content-Type: application/json
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
```
**Result**: ❌ 403 Forbidden
### Admin User - Any Address
```bash
POST /Trading/InitPrivyWallet
Authorization: Bearer {admin-jwt-token}
Content-Type: application/json
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
```
**Result**: ✅ Success (admin can initialize any address)
## Security Benefits
1. **Prevents Unauthorized Access**: Users cannot initialize wallets they don't own
2. **Admin Oversight**: Admins can manage any wallet for system administration
3. **Audit Trail**: All actions are logged for compliance and security monitoring
4. **Clear Authorization**: Explicit permission checks with meaningful error messages
5. **Secure Configuration**: Admin privileges configured via environment variables, not API calls

View File

@@ -81,5 +81,6 @@
"WorkerBundleBacktest": false, "WorkerBundleBacktest": false,
"WorkerBalancesTracking": false, "WorkerBalancesTracking": false,
"WorkerNotifyBundleBacktest": false, "WorkerNotifyBundleBacktest": false,
"AdminUsers": "",
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@@ -0,0 +1,55 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Shared;
public interface IAdminConfigurationService
{
bool IsUserAdmin(string userName);
List<string> GetAdminUserNames();
}
public class AdminConfigurationService : IAdminConfigurationService
{
private readonly IConfiguration _configuration;
private readonly ILogger<AdminConfigurationService> _logger;
public AdminConfigurationService(IConfiguration configuration, ILogger<AdminConfigurationService> logger)
{
_configuration = configuration;
_logger = logger;
}
public bool IsUserAdmin(string userName)
{
if (string.IsNullOrEmpty(userName))
{
return false;
}
var adminUserNames = GetAdminUserNames();
var isAdmin = adminUserNames.Contains(userName, StringComparer.OrdinalIgnoreCase);
if (isAdmin)
{
_logger.LogInformation("User {UserName} has admin privileges", userName);
}
return isAdmin;
}
public List<string> GetAdminUserNames()
{
var adminUsers = _configuration["AdminUsers"];
if (string.IsNullOrEmpty(adminUsers))
{
_logger.LogDebug("No admin users configured. Set AdminUsers environment variable.");
return new List<string>();
}
return adminUsers.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(u => u.Trim())
.Where(u => !string.IsNullOrEmpty(u))
.ToList();
}
}

View File

@@ -287,6 +287,9 @@ public static class ApiBootstrap
services.AddSingleton<IMessengerService, MessengerService>(); services.AddSingleton<IMessengerService, MessengerService>();
services.AddSingleton<IDiscordService, DiscordService>(); services.AddSingleton<IDiscordService, DiscordService>();
// Admin services
services.AddSingleton<IAdminConfigurationService, AdminConfigurationService>();
return services; return services;
} }