Skip to content

feat: [TESIS-41] add row-level multi-tenancy ORM scoping - #32

Open
TomasMartin2004 wants to merge 3 commits into
masterfrom
TESIS-41-multi-tenancy-orm-scoping
Open

feat: [TESIS-41] add row-level multi-tenancy ORM scoping#32
TomasMartin2004 wants to merge 3 commits into
masterfrom
TESIS-41-multi-tenancy-orm-scoping

Conversation

@TomasMartin2004

@TomasMartin2004 TomasMartin2004 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Ticket de Jira

https://proyectofinalfrlp.atlassian.net/browse/TESIS-41


Descripción

Implementa el aislamiento de datos row-level a nivel ORM. El company_id validado del JWT se inyecta en un contexto global por request (Current) y un concern CompanyScoped agrega un default scope WHERE company_id = ? a todos los modelos con tenencia, además de forzar el company_id del contexto al crear registros (ignorando cualquier valor del payload).

Decisión de diseño: el ADR-003 mostraba el default_scope directo en ApplicationRecord, pero eso rompe las tablas globales sin company_id (companies, services). Se optó por un concern opt-in por modelo — exactamente lo que la card pide al exceptuar "modelos base de configuración global" — y se actualizaron ADR-003 y architecture.md para reflejarlo.

  • Current (ActiveSupport::CurrentAttributes) con company_id y user, seteado en ApplicationController#set_current_tenant a partir del usuario autenticado.
  • Concern CompanyScoped: default scope por tenant + before_validation on: :create que fuerza company_id = Current.company_id.
  • Incluido en User, Warehouse y CompanyIntegration (Company y Service quedan fuera: son el tenant y plantillas globales).
  • rescue_from ActiveRecord::RecordNotFound → 404 JSON en ApplicationController: buscar un registro de otra empresa por id devuelve 404, nunca 403.
  • Escape hatch para workers/seeds/consola: Model.unscoped (documentado en ADR-003 y architecture.md). Con Current sin setear (login, seeds) el scope no aplica.

Evidencia visual

image image

Cómo probar

  1. bundle exec rspec spec/models/concerns/company_scoped_spec.rb — cubre: listado scopeado por tenant, find cross-tenant → RecordNotFound, create ignora company_id del payload, unscoped ve todo, contexto nil sin scope.
  2. En rails console:
    • Current.company_id = Company.first.id; Warehouse.all.to_sql → incluye WHERE "warehouses"."company_id" = ....
    • Warehouse.find(<id de otra empresa>)ActiveRecord::RecordNotFound.
    • Warehouse.create!(name: 'X', zip_code: '1', address: 'Y', company_id: <otra empresa>) → queda con el company_id del contexto.
    • Warehouse.unscoped.count → ve todos los registros.
  3. Vía API: login (POST /api/v1/auth/login), usar el token; cualquier endpoint futuro queda scopeado automáticamente por el company_id del JWT.

Impacto y consideraciones

¿Introduce breaking changes?
No — con Current sin setear el comportamiento es idéntico al actual (seeds, specs existentes y login no cambian).

¿Requiere nuevas variables de entorno?
No.

¿Afecta la arquitectura o genera un nuevo patrón?
Sí — todo modelo nuevo con company_id debe incluir CompanyScoped. ADR-003 y docs/guidelines/architecture.md actualizados en este PR.


TomasMartin2004 and others added 3 commits July 9, 2026 20:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y injection

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed scoping

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants