using Microsoft.Data.SqlClient; namespace MxNativeClient; public sealed record GalaxyUserProfile( int UserProfileId, string UserProfileName, Guid UserGuid, string DefaultSecurityGroup, int? InTouchAccessLevel, IReadOnlyList Roles); public sealed class GalaxyRepositoryUserResolver { private const string DefaultConnectionString = "Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True"; private readonly string _connectionString; public GalaxyRepositoryUserResolver(string connectionString = DefaultConnectionString) { ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); _connectionString = connectionString; } public async Task ResolveUserProfileIdByGuidAsync( Guid userGuid, CancellationToken cancellationToken = default) { GalaxyUserProfile profile = await ResolveByGuidAsync(userGuid, cancellationToken).ConfigureAwait(false); return profile.UserProfileId; } public async Task ResolveByGuidAsync( Guid userGuid, CancellationToken cancellationToken = default) { await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(cancellationToken).ConfigureAwait(false); await using var command = connection.CreateCommand(); command.CommandText = UserByGuidSql; command.Parameters.AddWithValue("@userGuid", userGuid); await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { throw new KeyNotFoundException($"Galaxy user GUID {userGuid} was not found in dbo.user_profile."); } return ReadProfile(reader); } public async Task ResolveByNameAsync( string userName, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(userName); await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(cancellationToken).ConfigureAwait(false); await using var command = connection.CreateCommand(); command.CommandText = UserByNameSql; command.Parameters.AddWithValue("@userName", userName); await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { throw new KeyNotFoundException($"Galaxy user {userName} was not found in dbo.user_profile."); } return ReadProfile(reader); } private static GalaxyUserProfile ReadProfile(SqlDataReader reader) { return new GalaxyUserProfile( reader.GetInt32(0), reader.GetString(1), reader.GetGuid(2), reader.GetString(3), reader.IsDBNull(4) ? null : reader.GetInt32(4), reader.IsDBNull(5) ? [] : ParseRoleBlob(reader.GetString(5))); } private static IReadOnlyList ParseRoleBlob(string rolesText) { if (!rolesText.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || rolesText.Length <= 2) { return []; } byte[] bytes = Convert.FromHexString(rolesText[2..]); List roles = []; for (int offset = 0; offset + 3 < bytes.Length; offset++) { List chars = []; int cursor = offset; while (cursor + 1 < bytes.Length) { char c = (char)(bytes[cursor] | (bytes[cursor + 1] << 8)); if (c == '\0') { break; } if (c < 0x20 || c > 0x7e) { chars.Clear(); break; } chars.Add(c); cursor += 2; } if (chars.Count < 2 || cursor + 1 >= bytes.Length || bytes[cursor] != 0 || bytes[cursor + 1] != 0) { continue; } string role = new(chars.ToArray()); if (!roles.Contains(role, StringComparer.OrdinalIgnoreCase)) { roles.Add(role); } offset = cursor; } return roles; } private const string UserSelectSql = """ SELECT TOP (1) user_profile_id, user_profile_name, user_guid, default_security_group, intouch_access_level, CONVERT(nvarchar(max), roles) AS roles_text FROM dbo.user_profile """; private const string UserByGuidSql = UserSelectSql + "\nWHERE user_guid = @userGuid\nORDER BY user_profile_id"; private const string UserByNameSql = UserSelectSql + "\nWHERE user_profile_name = @userName\nORDER BY user_profile_id"; }