consolidate CommonSecurity to Common.dll

This commit is contained in:
Alex Bezdieniezhnykh
2025-06-13 23:06:48 +03:00
parent 904bc688ca
commit 8aa2f563a4
58 changed files with 362 additions and 151 deletions
@@ -1,24 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" />
<PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="MessagePack" Version="3.1.0" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.5" />
<PackageReference Include="NetMQ" Version="4.0.1.13" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
</ItemGroup>
</Project>
@@ -1,16 +0,0 @@
using CommandLine;
using MessagePack;
namespace Azaion.CommonSecurity.DTO;
[MessagePackObject]
public class ApiCredentials : EventArgs
{
[Key(nameof(Email))]
[Option('e', "email", Required = true, HelpText = "User Email")]
public string Email { get; set; } = null!;
[Key(nameof(Password))]
[Option('p', "pass", Required = true, HelpText = "User Password")]
public string Password { get; set; } = null!;
}
@@ -1,7 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
internal class BusinessExceptionDto
{
public int ErrorCode { get; set; }
public string Message { get; set; } = string.Empty;
}
@@ -1,51 +0,0 @@
using MessagePack;
namespace Azaion.CommonSecurity.DTO.Commands;
[MessagePackObject]
public class RemoteCommand(CommandType commandType, byte[]? data = null, string? message = null)
{
[Key("CommandType")]
public CommandType CommandType { get; set; } = commandType;
[Key("Data")]
public byte[]? Data { get; set; } = data;
[Key("Message")]
public string? Message { get; set; } = message;
public static RemoteCommand Create(CommandType commandType) =>
new(commandType);
public static RemoteCommand Create<T>(CommandType commandType, T data, string? message = null) where T : class =>
new(commandType, MessagePackSerializer.Serialize(data), message);
public override string ToString() => $"({CommandType.ToString().ToUpper()}: Data: {Data?.Length ?? 0} bytes. Message: {Message})";
}
[MessagePackObject]
public class LoadFileData(string filename, string? folder = null )
{
[Key(nameof(Folder))]
public string? Folder { get; set; } = folder;
[Key(nameof(Filename))]
public string Filename { get; set; } = filename;
}
public enum CommandType
{
None = 0,
Ok = 3,
Login = 10,
Load = 20,
DataBytes = 25,
Inference = 30,
InferenceData = 35,
StopInference = 40,
AIAvailabilityCheck = 80,
AIAvailabilityResult = 85,
Error = 90,
Exit = 100,
}
@@ -1,16 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public class DirectoriesConfig
{
public string ApiResourcesDirectory { get; set; } = null!;
public string VideosDirectory { get; set; } = null!;
public string LabelsDirectory { get; set; } = null!;
public string ImagesDirectory { get; set; } = null!;
public string ResultsDirectory { get; set; } = null!;
public string ThumbnailsDirectory { get; set; } = null!;
public string GpsSatDirectory { get; set; } = null!;
public string GpsRouteDirectory { get; set; } = null!;
}
@@ -1,22 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public abstract class ExternalClientConfig
{
public string ZeroMqHost { get; set; } = "";
public int ZeroMqPort { get; set; }
}
public class LoaderClientConfig : ExternalClientConfig
{
public string ApiUrl { get; set; } = null!;
}
public class InferenceClientConfig : ExternalClientConfig
{
public string ApiUrl { get; set; } = null!;
}
public class GpsDeniedClientConfig : ExternalClientConfig
{
public int ZeroMqReceiverPort { get; set; }
}
@@ -1,9 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public class HardwareInfo
{
public string CPU { get; set; } = null!;
public string GPU { get; set; } = null!;
public string MacAddress { get; set; } = null!;
public string Memory { get; set; } = null!;
}
@@ -1,7 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public static class EnumerableExtensions
{
public static bool In<T>(this T obj, params T[] objects) =>
objects.Contains(obj);
}
-9
View File
@@ -1,9 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public class InitConfig
{
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
}
@@ -1,6 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public class LoginResponse
{
public string Token { get; set; } = null!;
}
-17
View File
@@ -1,17 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public enum RoleEnum
{
None = 0,
Operator = 10, //only annotator is available. Could send annotations to queue.
Validator = 20, //annotator + dataset explorer. This role allows to receive annotations from the queue.
CompanionPC = 30,
Admin = 40, //
ApiAdmin = 1000 //everything
}
public static class RoleEnumExtensions
{
public static bool IsValidator(this RoleEnum role) =>
role.In(RoleEnum.Validator, RoleEnum.Admin, RoleEnum.ApiAdmin);
}
-21
View File
@@ -1,21 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public class User
{
public string Id { get; set; } = "";
public string Email { get; set; } = "";
public RoleEnum Role { get; set; }
public UserConfig? UserConfig { get; set; } = null!;
}
public class UserConfig
{
public UserQueueOffsets? QueueOffsets { get; set; } = new();
}
public class UserQueueOffsets
{
public ulong AnnotationsOffset { get; set; }
public ulong AnnotationsConfirmOffset { get; set; }
public ulong AnnotationsCommandsOffset { get; set; }
}
@@ -1,3 +0,0 @@
namespace Azaion.CommonSecurity.Exceptions;
public class BusinessException(string message) : Exception(message);
@@ -1,78 +0,0 @@
using Azaion.CommonSecurity.DTO;
using Newtonsoft.Json;
namespace Azaion.CommonSecurity;
public class SecurityConstants
{
public const string CONFIG_PATH = "config.json";
private const string DEFAULT_API_URL = "https://api.azaion.com";
#region ExternalClientsConfig
private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1";
private const int DEFAULT_ZMQ_LOADER_PORT = 5025;
public const string EXTERNAL_LOADER_PATH = "azaion-loader.exe";
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5227;
# region Cache keys
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
public const string HARDWARE_INFO_KEY = "HardwareInfo";
# endregion
public static readonly InitConfig DefaultInitConfig = new()
{
LoaderClientConfig = new LoaderClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST,
ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT,
ApiUrl = DEFAULT_API_URL
},
InferenceClientConfig = new InferenceClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
ApiUrl = DEFAULT_API_URL
},
GpsDeniedClientConfig = new GpsDeniedClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT
},
DirectoriesConfig = new DirectoriesConfig
{
ApiResourcesDirectory = ""
}
};
#endregion ExternalClientsConfig
public static InitConfig ReadInitConfig()
{
try
{
if (!File.Exists(CONFIG_PATH))
throw new FileNotFoundException(CONFIG_PATH);
var configStr = File.ReadAllText(CONFIG_PATH);
var config = JsonConvert.DeserializeObject<InitConfig>(configStr);
return config ?? DefaultInitConfig;
}
catch (Exception e)
{
Console.WriteLine(e);
return DefaultInitConfig;
}
}
}
@@ -1,123 +0,0 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using Azaion.CommonSecurity.DTO;
using Newtonsoft.Json;
namespace Azaion.CommonSecurity.Services;
public interface IAzaionApi
{
ApiCredentials Credentials { get; }
User CurrentUser { get; }
void UpdateOffsets(UserQueueOffsets offsets);
//Stream GetResource(string filename, string folder);
}
public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
{
private string _jwtToken = null!;
const string APP_JSON = "application/json";
public ApiCredentials Credentials => credentials;
public User CurrentUser
{
get
{
var user = cache.GetFromCache(SecurityConstants.CURRENT_USER_CACHE_KEY,
() => Get<User>("currentUser"));
if (user == null)
throw new Exception("Can't get current user");
return user;
}
}
public void UpdateOffsets(UserQueueOffsets offsets)
{
Put($"/users/queue-offsets/set", new
{
Email = CurrentUser.Email,
Offsets = offsets
});
}
private HttpResponseMessage Send(HttpRequestMessage request)
{
if (string.IsNullOrEmpty(_jwtToken))
Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
var response = client.Send(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
response = client.Send(request);
}
if (response.IsSuccessStatusCode)
return response;
var stream = response.Content.ReadAsStream();
var content = new StreamReader(stream).ReadToEnd();
if (response.StatusCode == HttpStatusCode.Conflict)
{
var result = JsonConvert.DeserializeObject<BusinessExceptionDto>(content);
throw new Exception($"Failed: {response.StatusCode}! Error Code: {result?.ErrorCode}. Message: {result?.Message}");
}
throw new Exception($"Failed: {response.StatusCode}! Result: {content}");
}
private T? Get<T>(string url)
{
var response = Send(new HttpRequestMessage(HttpMethod.Get, url));
var stream = response.Content.ReadAsStream();
var json = new StreamReader(stream).ReadToEnd();
return JsonConvert.DeserializeObject<T>(json);
}
private void Put<T>(string url, T obj)
{
Send(new HttpRequestMessage(HttpMethod.Put, url)
{
Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON)
});
}
private void Authorize()
{
try
{
if (string.IsNullOrEmpty(credentials.Email) || credentials.Password.Length == 0)
throw new Exception("Email or password is empty! Please do EnterCredentials first!");
var payload = new
{
email = credentials.Email,
password = credentials.Password
};
var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, APP_JSON);
var message = new HttpRequestMessage(HttpMethod.Post, "login") { Content = content };
var response = client.Send(message);
if (!response.IsSuccessStatusCode)
throw new Exception($"EnterCredentials failed: {response.StatusCode}");
var stream = response.Content.ReadAsStream();
var json = new StreamReader(stream).ReadToEnd();
var result = JsonConvert.DeserializeObject<LoginResponse>(json);
if (string.IsNullOrEmpty(result?.Token))
throw new Exception("JWT Token not found in response");
_jwtToken = result.Token;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
-27
View File
@@ -1,27 +0,0 @@
using LazyCache;
namespace Azaion.CommonSecurity.Services;
public interface ICache
{
T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null);
void Invalidate(string key);
}
public class MemoryCache : ICache
{
private readonly IAppCache _cache = new CachingService();
public T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null)
{
expiration ??= TimeSpan.FromHours(4);
return _cache.GetOrAdd(key, entry =>
{
var result = fetchFunc();
entry.AbsoluteExpirationRelativeToNow = expiration;
return result;
});
}
public void Invalidate(string key) => _cache.Remove(key);
}
@@ -1,103 +0,0 @@
using System.Diagnostics;
using System.Text;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.DTO.Commands;
using MessagePack;
using NetMQ;
using NetMQ.Sockets;
using Serilog;
using Exception = System.Exception;
namespace Azaion.CommonSecurity.Services;
public class LoaderClient(LoaderClientConfig config, ILogger logger, CancellationToken ct = default) : IDisposable
{
private readonly DealerSocket _dealer = new();
private readonly Guid _clientId = Guid.NewGuid();
public void StartClient()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.EXTERNAL_LOADER_PATH,
Arguments = $"--port {config.ZeroMqPort} --api {config.ApiUrl}",
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) =>
{
if (e.Data != null) Console.WriteLine(e.Data);
};
process.ErrorDataReceived += (_, e) =>
{
if (e.Data != null) Console.WriteLine(e.Data);
};
process.Start();
}
catch (Exception e)
{
logger.Error(e.Message);
throw;
}
}
public void Connect()
{
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{config.ZeroMqHost}:{config.ZeroMqPort}");
}
public void Login(ApiCredentials credentials)
{
var result = SendCommand(RemoteCommand.Create(CommandType.Login, credentials));
if (result.CommandType != CommandType.Ok)
throw new Exception(result.Message);
}
public MemoryStream LoadFile(string filename, string folder)
{
var result = SendCommand(RemoteCommand.Create(CommandType.Load, new LoadFileData(filename, folder)));
if (result.Data?.Length == 0)
throw new Exception($"Can't load {filename}. Returns 0 bytes");
return new MemoryStream(result.Data!);
}
private RemoteCommand SendCommand(RemoteCommand command, int retryCount = 50, int retryDelayMs = 800)
{
try
{
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum++ < retryCount)
{
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(retryDelayMs), out var bytes))
continue;
var res = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
if (res.CommandType == CommandType.Error)
throw new Exception(res.Message);
return res;
}
throw new Exception($"Sent {command} {retryCount} times, with wait time {retryDelayMs}ms for each call. No response from client.");
}
catch (Exception e)
{
logger.Error(e, e.Message);
throw;
}
}
public void Stop()
{
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
}
public void Dispose()
{
_dealer.Dispose();
}
}