-
Notifications
You must be signed in to change notification settings - Fork 0
fix: [TESIS-82] close the auth and multi-tenancy gaps found by the module QA #89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1b3af95
7b116b2
d237240
0353ffa
4b6de72
86b5fa7
7b6cb68
87ea8f9
a53b91b
f596747
a3b024d
a200b13
46b9b29
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,28 @@ class MalformedParameterError < StandardError; end | |
|
|
||
| def index_action? = action_name == 'index' | ||
|
|
||
| # Devise valida el token (firma, vencimiento, denylist); acá se suma lo que el | ||
| # token no puede saber porque pudo cambiar después de emitido: si la empresa | ||
| # sigue activa y si la cuenta sigue aprobada. El JWT sigue siendo válido hasta | ||
| # vencer, y sin este chequeo la sesión seguía operando hasta 24 h después de | ||
| # la baja. | ||
| # | ||
| # Va sobre authenticate_user! y no en active_for_authentication? de Devise a | ||
| # propósito: ese hook corta con 401 cualquier request que traiga el token, | ||
| # también los que no exigen sesión (login, registro, tenant-config). Acá sólo | ||
| # corre donde se pide sesión. | ||
| def authenticate_user!(*) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Este chequeo también corre en El flujo: dan de baja a Norte → el próximo request da 401 → el front hace Propuesta: saltear el rechazo en |
||
| super | ||
| refusal = session_refusal | ||
| render json: { error: refusal }, status: :unauthorized if refusal | ||
| end | ||
|
|
||
| def session_refusal | ||
| return 'The company of this account is not active' unless current_company.is_active? | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: ahora cada request autenticado hace un |
||
|
|
||
| 'This account is pending approval' unless current_user.approved? | ||
| end | ||
|
|
||
| def set_current_tenant | ||
| Current.company_id = current_user&.company_id | ||
| Current.user = current_user | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Api | ||
| module V1 | ||
| module Auth | ||
| # Límite de intentos para los endpoints públicos de auth: login y registro. | ||
| # | ||
| # Sin esto se podían probar contraseñas sin freno: después de 30 incorrectas | ||
| # seguidas, la correcta entraba igual. Se limita por IP y no con el | ||
| # :lockable de Devise, que bloquea la cuenta: con él, cualquiera podría | ||
| # dejar afuera a otro usuario tipeando mal su password a propósito. | ||
| # | ||
| # Cada endpoint cuenta distinto: | ||
| # - Login: sólo los intentos fallidos (before_action | ||
| # :refuse_exhausted_attempts + count_failed_attempt en la rama del 401). | ||
| # El que entra no está adivinando nada, y contar los exitosos dejaba | ||
| # afuera al undécimo operario de un depósito detrás de un mismo NAT | ||
| # aunque pusiera bien su password. | ||
| # - Registro: todos, con el rate_limit de Rails. Cada pedido crea una | ||
| # solicitud, y no hay un «pedido exitoso» que se repita en el uso normal. | ||
| # | ||
| # El contador vive en la cache de la app, que en producción es Solid Cache y | ||
| # la comparten todos los procesos. En test la cache es :null_store y el | ||
| # límite nunca se alcanza, así los specs que loguean muchas veces no chocan | ||
| # con él; el spec del límite le da una cache de verdad a propósito. | ||
| module AttemptLimit | ||
| extend ActiveSupport::Concern | ||
|
|
||
| MAX_ATTEMPTS = 10 | ||
| WINDOW = 3.minutes | ||
|
|
||
| private | ||
|
|
||
| # Una vez agotados se rechaza sin mirar la password, también la correcta: | ||
| # si se la evaluara, el que prueba contraseñas seguiría probando y la | ||
| # correcta le daría el token igual. | ||
| def refuse_exhausted_attempts | ||
| render_too_many_attempts if failed_attempts >= MAX_ATTEMPTS | ||
| end | ||
|
|
||
| # La ventana es fija: el increment de la cache conserva el vencimiento | ||
| # que tomó la entrada con el primer fallo. | ||
| def count_failed_attempt | ||
| attempt_store.increment(failed_attempts_key, 1, expires_in: WINDOW) | ||
| end | ||
|
|
||
| # `raw: true` porque el valor lo escribió increment, que en algunos stores | ||
| # guarda el entero sin serializar. | ||
| def failed_attempts | ||
| attempt_store.read(failed_attempts_key, raw: true).to_i | ||
| end | ||
|
|
||
| def failed_attempts_key | ||
| "auth-failed-attempts:#{request.remote_ip}" | ||
| end | ||
|
|
||
| def attempt_store | ||
| self.class.cache_store | ||
| end | ||
|
|
||
| def render_too_many_attempts | ||
| response.set_header('Retry-After', WINDOW.to_i.to_s) | ||
| render json: { error: 'Too many attempts, try again later' }, status: :too_many_requests | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class CompanyIntegrationPolicy < ApplicationPolicy | ||
| # El flag `integrations` de la empresa sólo lo aplicaba el front: una empresa | ||
| # sin la feature configuraba integraciones llamando al endpoint directo (QA de | ||
| # TESIS-82). Se compara con `== true` en Company#feature_enabled?, igual que | ||
| # el front, así los dos deciden lo mismo. | ||
| def update? | ||
| user.present? && user.company.feature_enabled?(:integrations) | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
El mismo
require+permitque daba 500 sigue enproducts_controller.rb:123,stock_transfers_controller.rb:69,product_mappings_controller.rb:74yorders_controller.rb:172(por ejemplo,POST /productscon{"product": "x"}→ 500). Ya está en las limitaciones conocidas; propongo abrir una card para migrarlos todos aparams.expect, así el hallazgo 7 no queda cerrado a medias.