bemade-addons/bemade_sports_clinic/tests/test_security_integration.py

312 lines
13 KiB
Python
Raw Normal View History

from odoo.tests import HttpCase, tagged
from odoo.exceptions import AccessError
from odoo import Command, fields
import json
@tagged("-at_install", "post_install")
class TestSecurityIntegration(HttpCase):
"""Integration tests for the sports clinic security features"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create organization and team
cls.organization = cls.env['sports.organization'].create({
'name': 'Test Security Organization',
})
cls.team = cls.env['sports.team'].create({
'name': 'Test Security Team',
'organization_id': cls.organization.id,
})
# Create some patients/players
cls.patient1 = cls.env['sports.patient'].create({
'first_name': 'Security',
'last_name': 'Test Patient',
'birthdate': '2005-01-01',
'team_ids': [(4, cls.team.id)],
})
# Create an active injury for patient1
cls.existing_injury = cls.env['sports.patient.injury'].create({
'patient_id': cls.patient1.id,
'team_id': cls.team.id,
'diagnosis': 'Security Test Injury',
'stage': 'active',
'injury_date': fields.Date.today(),
'internal_notes': 'These are internal notes for security testing',
'external_notes': 'These are external notes for security testing',
'parental_consent': 'yes',
})
# Create a second team that will not have our test users as staff
cls.restricted_team = cls.env['sports.team'].create({
'name': 'Restricted Team',
'organization_id': cls.organization.id,
})
cls.restricted_patient = cls.env['sports.patient'].create({
'first_name': 'Restricted',
'last_name': 'Patient',
'birthdate': '2006-02-02',
'team_ids': [(4, cls.restricted_team.id)],
})
cls.restricted_injury = cls.env['sports.patient.injury'].create({
'patient_id': cls.restricted_patient.id,
'team_id': cls.restricted_team.id,
'diagnosis': 'Restricted Injury',
'stage': 'active',
'injury_date': fields.Date.today(),
})
# Create users with different roles
# 1. Therapist (treatment professional)
cls.therapist_partner = cls.env['res.partner'].create({
'name': 'Security Therapist',
'email': 'security.therapist@example.com',
})
cls.therapist_user = cls.env['res.users'].with_context(no_reset_password=True).create({
'partner_id': cls.therapist_partner.id,
'login': 'security.therapist@example.com',
'password': 'therapist123',
'name': cls.therapist_partner.name,
'groups_id': [
Command.link(cls.env.ref('base.group_portal').id),
Command.link(cls.env.ref('bemade_sports_clinic.group_portal_treatment_professional').id),
]
})
# 2. Coach
cls.coach_partner = cls.env['res.partner'].create({
'name': 'Security Coach',
'email': 'security.coach@example.com',
})
cls.coach_user = cls.env['res.users'].with_context(no_reset_password=True).create({
'partner_id': cls.coach_partner.id,
'login': 'security.coach@example.com',
'password': 'coach123',
'name': cls.coach_partner.name,
'groups_id': [
Command.link(cls.env.ref('base.group_portal').id),
Command.link(cls.env.ref('bemade_sports_clinic.group_portal_team_coach').id),
]
})
# Create team staff entries for the main test team only
cls.env['sports.team.staff'].create({
'team_id': cls.team.id,
'partner_id': cls.therapist_partner.id,
# Role therapist automatically grants treatment professional status
'role': 'therapist',
'user_id': cls.therapist_user.id,
})
cls.env['sports.team.staff'].create({
'team_id': cls.team.id,
'partner_id': cls.coach_partner.id,
# Role coach doesn't grant treatment professional status
'role': 'coach',
'user_id': cls.coach_user.id,
})
def test_01_field_level_security_for_therapist(self):
"""Test field-level security validation for therapist users"""
# Login as therapist
self.authenticate('security.therapist@example.com', 'therapist123')
# Therapist should be able to access the patient injury page with internal notes
injury_response = self.url_open(f'/my/player/injury?injury_id={self.existing_injury.id}')
self.assertEqual(injury_response.status_code, 200)
# Verify that therapist can see internal notes field
self.assertIn('Internal Notes', injury_response.text)
self.assertIn('These are internal notes for security testing', injury_response.text)
def test_02_field_level_security_for_coach(self):
"""Test field-level security validation for coach users"""
# Login as coach
self.authenticate('security.coach@example.com', 'coach123')
# Coach should be able to access the patient injury page but not see internal notes
injury_response = self.url_open(f'/my/player/injury?injury_id={self.existing_injury.id}')
self.assertEqual(injury_response.status_code, 200)
# Verify that coach cannot see internal notes field or its content
self.assertNotIn('These are internal notes for security testing', injury_response.text)
# Check if the form access properly restricts the parental consent field
injury_form_response = self.url_open(f'/my/patient/injury/new?patient_id={self.patient1.id}')
self.assertEqual(injury_form_response.status_code, 200)
# Verify parental consent field is not shown to coaches
self.assertNotIn('id="parental_consent"', injury_form_response.text)
def test_03_therapist_cannot_access_unauthorized_team(self):
"""Test that therapists cannot access teams they're not staff of"""
# Login as therapist
self.authenticate('security.therapist@example.com', 'therapist123')
# 1. Test that therapist can access authorized team
authorized_team_response = self.url_open(f'/my/team?team_id={self.team.id}')
self.assertEqual(authorized_team_response.status_code, 200)
self.assertIn(self.team.name, authorized_team_response.text)
# 2. Test that therapist cannot access unauthorized team
# This might redirect to a permission error page or to the teams list
restricted_team_response = self.url_open(f'/my/team?team_id={self.restricted_team.id}')
# Should either be an error page or not contain the restricted team name
if restricted_team_response.status_code == 200:
self.assertNotIn(self.restricted_team.name, restricted_team_response.text)
else:
self.assertIn(restricted_team_response.status_code, [403, 404])
def test_04_coach_cannot_access_unauthorized_team(self):
"""Test that coaches cannot access teams they're not staff of"""
# Login as coach
self.authenticate('security.coach@example.com', 'coach123')
# 1. Test that coach can access authorized team
authorized_team_response = self.url_open(f'/my/team?team_id={self.team.id}')
self.assertEqual(authorized_team_response.status_code, 200)
self.assertIn(self.team.name, authorized_team_response.text)
# 2. Test that coach cannot access unauthorized team
# This might redirect to a permission error page or to the teams list
restricted_team_response = self.url_open(f'/my/team?team_id={self.restricted_team.id}')
# Should either be an error page or not contain the restricted team name
if restricted_team_response.status_code == 200:
self.assertNotIn(self.restricted_team.name, restricted_team_response.text)
else:
self.assertIn(restricted_team_response.status_code, [403, 404])
def test_05_therapist_cannot_modify_unauthorized_injury(self):
"""Test that therapists cannot modify injuries from teams they're not staff of"""
# Login as therapist
self.authenticate('security.therapist@example.com', 'therapist123')
# Try to modify a restricted injury
# Prepare injury update data
update_data = {
'csrf_token': self.csrf_token(),
'injury_id': self.restricted_injury.id,
'diagnosis': 'Attempted Unauthorized Update',
'external_notes': 'This update should fail',
}
# This should fail or redirect
update_response = self.url_open(
'/my/patient/injury/update',
data=update_data,
timeout=30,
)
# Refresh the record from database to check if changes were saved
self.restricted_injury.invalidate_cache()
# Verify no changes were made
self.assertNotEqual(self.restricted_injury.diagnosis, 'Attempted Unauthorized Update')
def test_06_coach_cannot_modify_any_injury(self):
"""Test that coaches cannot modify any injury (they can only create)"""
# Login as coach
self.authenticate('security.coach@example.com', 'coach123')
# Try to modify an injury from their team
# Prepare injury update data
update_data = {
'csrf_token': self.csrf_token(),
'injury_id': self.existing_injury.id,
'diagnosis': 'Coach Attempted Update',
'external_notes': 'This update should fail',
}
# This should fail or redirect
update_response = self.url_open(
'/my/patient/injury/update',
data=update_data,
timeout=30,
)
# Refresh the record from database to check if changes were saved
self.existing_injury.invalidate_cache()
# Verify no changes were made
self.assertNotEqual(self.existing_injury.diagnosis, 'Coach Attempted Update')
def test_07_permission_escalation_prevention(self):
"""Test prevention of permission escalation through direct model access"""
# Login as coach to test permission boundaries
self.authenticate('security.coach@example.com', 'coach123')
# Try to directly call the server model methods that should be protected
# We'll use a JSON-RPC call to simulate attempting to escalate permissions
# Try to create a direct JSON-RPC call to update an injury
json_data = {
"jsonrpc": "2.0",
"method": "call",
"params": {
"model": "sports.patient.injury",
"method": "write",
"args": [
self.existing_injury.id,
{"diagnosis": "Direct API Hack Attempt"}
],
"kwargs": {}
},
"id": 1
}
# This should fail with an error code
headers = {"Content-Type": "application/json"}
response = self.url_open(
'/web/dataset/call_kw',
data=json.dumps(json_data),
headers=headers
)
# Parse JSON response and check for error
response_data = json.loads(response.text)
# Either access should be denied or the method should fail
self.assertTrue(
'error' in response_data or
not response_data.get('result', False)
)
# Verify the injury wasn't actually updated
self.existing_injury.invalidate_cache()
self.assertNotEqual(self.existing_injury.diagnosis, "Direct API Hack Attempt")
def test_08_csrf_protection(self):
"""Test CSRF protection for form submissions"""
# Login as therapist
self.authenticate('security.therapist@example.com', 'therapist123')
# Attempt a form submission without a valid CSRF token
invalid_data = {
'csrf_token': 'invalid_token',
'injury_id': self.existing_injury.id,
'diagnosis': 'CSRF Attack',
'external_notes': 'This should fail due to invalid CSRF token',
}
# This should fail with a 400 error or redirect to form
update_response = self.url_open(
'/my/patient/injury/update',
data=invalid_data,
timeout=30,
)
# Check the injury record - it should not be updated
self.existing_injury.invalidate_cache()
self.assertNotEqual(self.existing_injury.diagnosis, 'CSRF Attack')