"""IDOR / cross-tenant access control tests.
Verifies that a user in Company A cannot read Company B's data via
company_id parameters (URL or query string), and that the tenancy
helpers in app/utils/tenancy.py enforce membership.
"""
import os
import sys
import unittest
import json
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
os.environ.setdefault('DATABASE_URL', 'sqlite:///test_idor.db')
os.environ.setdefault('SECRET_KEY', 'test-secret-key-for-testing')
from app import create_app
from app.models import db, User, Company, UserCompany
class IDORTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app = create_app()
cls.app.config['TESTING'] = True
cls.app.config['RATELIMIT_ENABLED'] = False
cls.client = cls.app.test_client()
with cls.app.app_context():
db.drop_all()
db.create_all()
company_a = Company(name='IDOR Test Co A')
company_b = Company(name='IDOR Test Co B')
db.session.add_all([company_a, company_b])
db.session.flush()
user_a = User(email='user_a@idor.test', full_name='User A')
user_a.set_password('UserAPassword1!')
super_admin = User(
email='admin@idor.test', full_name='Super', role='super_admin'
)
super_admin.set_password('AdminPassword1!')
db.session.add_all([user_a, super_admin])
db.session.flush()
db.session.add(UserCompany(
user_id=user_a.id, company_id=company_a.id, role='owner'
))
db.session.commit()
cls.company_a_id = company_a.id
cls.company_b_id = company_b.id
cls.user_a_id = user_a.id
def _login(self, email, password):
resp = self.client.post(
'/api/auth/login',
data=json.dumps({'email': email, 'password': password}),
content_type='application/json',
)
self.assertEqual(resp.status_code, 200, resp.get_data(as_text=True))
def _logout(self):
with self.client.session_transaction() as sess:
sess.clear()
# ── unauthenticated ──────────────────────────────────────────────
def test_unauthenticated_kpis_denied(self):
self._logout()
resp = self.client.get(f'/dashboard/api/kpis/{self.company_a_id}')
self.assertIn(resp.status_code, (401, 302))
# ── tenancy helper semantics ─────────────────────────────────────
def test_user_in_company_helper(self):
from app.utils.tenancy import user_in_company
with self.app.app_context():
self.assertTrue(user_in_company(self.user_a_id, self.company_a_id))
self.assertFalse(user_in_company(self.user_a_id, self.company_b_id))
self.assertFalse(user_in_company(None, self.company_a_id))
self.assertFalse(user_in_company(self.user_a_id, None))
# ── cross-tenant API access ──────────────────────────────────────
def test_member_can_access_own_company(self):
self._login('user_a@idor.test', 'UserAPassword1!')
resp = self.client.get(f'/dashboard/api/kpis/{self.company_a_id}')
self.assertEqual(resp.status_code, 200)
self._logout()
def test_member_cannot_access_other_company(self):
"""User A must get 403 when requesting Company B's KPIs (IDOR)."""
self._login('user_a@idor.test', 'UserAPassword1!')
resp = self.client.get(f'/dashboard/api/kpis/{self.company_b_id}')
self.assertEqual(resp.status_code, 403)
self._logout()
def test_member_cannot_access_other_company_forecast(self):
self._login('user_a@idor.test', 'UserAPassword1!')
resp = self.client.get(f'/dashboard/api/forecast/{self.company_b_id}')
self.assertEqual(resp.status_code, 403)
self._logout()
def test_super_admin_bypasses_membership(self):
self._login('admin@idor.test', 'AdminPassword1!')
resp = self.client.get(f'/dashboard/api/kpis/{self.company_b_id}')
self.assertEqual(resp.status_code, 200)
self._logout()
def test_nonexistent_company_id_denied_for_member(self):
"""Random/guessed company IDs are rejected, not leaked."""
self._login('user_a@idor.test', 'UserAPassword1!')
resp = self.client.get('/dashboard/api/kpis/00000000-0000-0000-0000-000000000000')
self.assertEqual(resp.status_code, 403)
self._logout()
if __name__ == '__main__':
unittest.main()