Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Atlas Balance/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ npm run build

# Release Windows x64
cd "Atlas Balance"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\Build-Release.ps1" -Version V-01.02
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\Build-Release.ps1" -Version V-01.03

# Conectar a PostgreSQL
psql -h localhost -p 5433 -U app_user -d atlas_balance
Expand Down
2 changes: 1 addition & 1 deletion Atlas Balance/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ npm run build

# Release Windows x64
cd "Atlas Balance"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\Build-Release.ps1" -Version V-01.02
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\Build-Release.ps1" -Version V-01.03

# Conectar a PostgreSQL
psql -h localhost -p 5433 -U app_user -d atlas_balance
Expand Down
8 changes: 4 additions & 4 deletions Atlas Balance/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<PropertyGroup>
<Company>Atlas Labs</Company>
<Product>Atlas Balance</Product>
<Version>1.2.0</Version>
<AssemblyVersion>1.2.0.0</AssemblyVersion>
<FileVersion>1.2.0.0</FileVersion>
<InformationalVersion>V-01.02</InformationalVersion>
<Version>1.3.0</Version>
<AssemblyVersion>1.3.0.0</AssemblyVersion>
<FileVersion>1.3.0.0</FileVersion>
<InformationalVersion>V-01.03</InformationalVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
2 changes: 1 addition & 1 deletion Atlas Balance/README_RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Atlas Balance V-01.02 - release Windows x64
# Atlas Balance V-01.03 - release Windows x64

Este paquete es autonomo para servidor Windows: el frontend ya esta compilado, el backend y Watchdog van publicados self-contained y la base de datos se prepara desde el instalador.

Expand Down
2 changes: 1 addition & 1 deletion Atlas Balance/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
V-01.02
V-01.03
35 changes: 35 additions & 0 deletions Atlas Balance/backend/src/GestionCaja.API/ConfigurationDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,40 @@ namespace GestionCaja.API;

public static class ConfigurationDefaults
{
public const string GitHubOwner = "AtlasLabs797";
public const string GitHubRepository = "AtlasBalance";
public const string UpdateCheckUrl = "https://github.com/AtlasLabs797/AtlasBalance";

public static bool TryNormalizeUpdateCheckUrl(string? configuredUrl, out string normalizedUrl)
{
normalizedUrl = string.IsNullOrWhiteSpace(configuredUrl)
? UpdateCheckUrl
: configuredUrl.Trim();

if (!Uri.TryCreate(normalizedUrl, UriKind.Absolute, out var uri))
{
return false;
}

if (!uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
return false;
}

if (uri.Host.Equals("github.com", StringComparison.OrdinalIgnoreCase))
{
var segments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return segments.Length >= 2 &&
segments[0].Equals(GitHubOwner, StringComparison.OrdinalIgnoreCase) &&
segments[1].Equals(GitHubRepository, StringComparison.OrdinalIgnoreCase);
}

if (uri.Host.Equals("api.github.com", StringComparison.OrdinalIgnoreCase))
{
var expectedPrefix = $"/repos/{GitHubOwner}/{GitHubRepository}/";
return uri.AbsolutePath.StartsWith(expectedPrefix, StringComparison.OrdinalIgnoreCase);
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ public static class AuditActions
public const string Logout = "LOGOUT";
public const string LoginFailed = "LOGIN_FAILED";
public const string AccountLocked = "ACCOUNT_LOCKED";
public const string PasswordChanged = "PASSWORD_CHANGED";
public const string PasswordReset = "PASSWORD_RESET";
public const string RefreshTokenReuseDetected = "REFRESH_TOKEN_REUSE_DETECTED";
public const string CreateUsuario = "CREATE_USUARIO";
public const string UpdateUsuario = "UPDATE_USUARIO";
public const string DeleteUsuario = "DELETE_USUARIO";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace GestionCaja.API.Constants;

public static class AuthClaimNames
{
public const string SecurityStamp = "security_stamp";
public const string PasswordChangedAt = "password_changed_at";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace GestionCaja.API.Constants;

public static class SecurityPolicy
{
public const int MinPasswordLength = 12;

private static readonly HashSet<string> CommonPasswords = new(StringComparer.OrdinalIgnoreCase)
{
"admin",
"admin123",
"admin1234",
"atlasbalance",
"changeme",
"password",
"password123",
"qwerty123",
"welcome123"
};

public static bool TryValidatePassword(string? password, out string error)
{
if (string.IsNullOrWhiteSpace(password) || password.Length < MinPasswordLength)
{
error = $"La contraseña debe tener al menos {MinPasswordLength} caracteres";
return false;
}

var normalized = password.Trim();
if (CommonPasswords.Contains(normalized))
{
error = "La contraseña es demasiado comun";
return false;
}

if (normalized.Distinct().Count() == 1)
{
error = "La contraseña no puede repetir un solo caracter";
return false;
}

error = string.Empty;
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,41 @@ public async Task<IActionResult> Get(CancellationToken cancellationToken)
[HttpPut]
public async Task<IActionResult> Update([FromBody] UpdateConfiguracionRequest request, CancellationToken cancellationToken)
{
if (request is null)
{
return BadRequest(new { error = "Request invalido." });
}

if (request.Smtp is null || request.General is null || request.Dashboard is null)
{
return BadRequest(new { error = "Configuracion incompleta." });
}

if (request.Smtp.Port <= 0 || request.Smtp.Port > 65535)
{
return BadRequest(new { error = "Puerto SMTP inválido." });
}

if (!IsValidAppBaseUrl(request.General.AppBaseUrl))
{
return BadRequest(new { error = "La URL base debe ser absoluta y usar http o https." });
}

if (!ConfigurationDefaults.TryNormalizeUpdateCheckUrl(request.General.AppUpdateCheckUrl, out var updateCheckUrl))
{
return BadRequest(new { error = "La URL de actualizaciones debe apuntar al repositorio oficial de Atlas Balance en GitHub por HTTPS." });
}

if (!IsSafeAbsoluteDirectory(request.General.BackupPath))
{
return BadRequest(new { error = "La ruta de backups debe ser absoluta y no contener traversal." });
}

if (!IsSafeAbsoluteDirectory(request.General.ExportPath))
{
return BadRequest(new { error = "La ruta de exportaciones debe ser absoluta y no contener traversal." });
}

var userId = GetCurrentUserId();
var now = DateTime.UtcNow;
var config = await _dbContext.Configuraciones.ToListAsync(cancellationToken);
Expand All @@ -95,7 +125,7 @@ public async Task<IActionResult> Update([FromBody] UpdateConfiguracionRequest re
Upsert(config, "smtp_from", request.Smtp.From.Trim(), userId, now);

Upsert(config, "app_base_url", request.General.AppBaseUrl.Trim(), userId, now);
Upsert(config, "app_update_check_url", request.General.AppUpdateCheckUrl.Trim(), userId, now);
Upsert(config, "app_update_check_url", updateCheckUrl, userId, now);
Upsert(config, "backup_path", request.General.BackupPath.Trim(), userId, now);
Upsert(config, "export_path", request.General.ExportPath.Trim(), userId, now);
var exchangeApiKey = request.Exchange?.ApiKey;
Expand Down Expand Up @@ -205,6 +235,54 @@ private static int ParseInt(string value, int fallback)
return int.TryParse(value, out var parsed) ? parsed : fallback;
}

private static bool IsValidAppBaseUrl(string? value)
{
if (!Uri.TryCreate(value?.Trim(), UriKind.Absolute, out var uri))
{
return false;
}

return uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) ||
uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase);
}

private static bool IsSafeAbsoluteDirectory(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return false;
}

var trimmed = value.Trim();
if (trimmed.Contains("..", StringComparison.Ordinal))
{
return false;
}

if (!Path.IsPathRooted(trimmed) && !LooksLikeWindowsRootedPath(trimmed))
{
return false;
}

try
{
var fullPath = Path.GetFullPath(trimmed);
return !string.IsNullOrWhiteSpace(fullPath);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
{
return false;
}
}

private static bool LooksLikeWindowsRootedPath(string value)
{
return value.Length >= 3 &&
char.IsLetter(value[0]) &&
value[1] == ':' &&
(value[2] == '\\' || value[2] == '/');
}

private static Dictionary<string, string> RedactSensitiveConfig(IReadOnlyDictionary<string, string> source)
{
return source.ToDictionary(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ private static bool IsAllowedExportFile(string filePath, string exportRoot)
return false;
}

if (!IsExplicitlyRooted(exportRoot))
{
return false;
}

try
{
var fullFilePath = Path.GetFullPath(filePath);
Expand All @@ -231,4 +236,13 @@ private static string EnsureTrailingSeparator(string path)
? path
: $"{path}{Path.DirectorySeparatorChar}";
}

private static bool IsExplicitlyRooted(string path)
{
return Path.IsPathRooted(path) ||
(path.Length >= 3 &&
char.IsLetter(path[0]) &&
path[1] == ':' &&
(path[2] == '\\' || path[2] == '/'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,7 @@ private async Task<HashSet<Guid>> GetAllowedAccountIds(Actor actor, Cancellation
if (actor.IsAdmin) return [.. await _db.Cuentas.Select(c => c.Id).ToListAsync(ct)];
var perms = await _db.PermisosUsuario.Where(p => p.UsuarioId == actor.Id).ToListAsync(ct);
if (!perms.Any()) return [];
if (perms.Any(p => p.CuentaId is null && p.TitularId is null &&
(p.PuedeAgregarLineas || p.PuedeEditarLineas || p.PuedeEliminarLineas ||
p.PuedeImportar || p.PuedeVerDashboard)))
if (perms.Any(p => p.CuentaId is null && p.TitularId is null && GrantsDataAccess(p)))
{
return [.. await _db.Cuentas.Select(c => c.Id).ToListAsync(ct)];
}
Expand All @@ -616,9 +614,7 @@ private async Task<bool> CanViewTitular(Actor actor, Guid titularId, Cancellatio
return false;
}

if (perms.Any(p => p.CuentaId is null && p.TitularId is null &&
(p.PuedeAgregarLineas || p.PuedeEditarLineas || p.PuedeEliminarLineas ||
p.PuedeImportar || p.PuedeVerDashboard)))
if (perms.Any(p => p.CuentaId is null && p.TitularId is null && GrantsDataAccess(p)))
{
return true;
}
Expand All @@ -638,6 +634,9 @@ private async Task<bool> CanViewTitular(Actor actor, Guid titularId, Cancellatio
await _db.Cuentas.AnyAsync(c => c.TitularId == titularId && permittedCuentaIds.Contains(c.Id), ct);
}

private static bool GrantsDataAccess(PermisoUsuario permiso) =>
permiso.PuedeAgregarLineas || permiso.PuedeEditarLineas || permiso.PuedeEliminarLineas || permiso.PuedeImportar;

private async Task<Perm> GetPermission(Actor actor, Cuenta cuenta, CancellationToken ct)
{
if (actor.IsAdmin) return new Perm { CanAdd = true, CanEdit = true, CanDelete = true, EditableCols = null };
Expand Down
Loading
Loading