- Removed models/injury_models.py and all its references - Converted relational fields to character fields: - body_location_id → body_location - injury_type_id → injury_type - Updated portal templates to use text inputs instead of dropdowns - Updated controller code to process the new field formats - Removed related access rights from security CSV - Modified test files to accommodate the new structure This refactoring simplifies the data model by removing unnecessary classifications that were adding complexity without significant benefit. The direct text fields maintain the same functionality while reducing the database overhead and simplifying the UI.
311 lines
13 KiB
Python
311 lines
13 KiB
Python
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')
|