settings: set secure flag on cookies (#71880)

Tests fixes :
* force https scheme in webtest HTTP client
* add secure=True to call with the django HTTP client
* replace http scheme by https in URLs assertions,
* properly use response.form in tests directly using app.post, as CSRF checks on secure connection also test the Referrer
* manually add Referer header in other cases,
This commit is contained in:
Benjamin Dauvergne 2022-11-30 14:43:02 +01:00
parent 97a5ebf63a
commit d8d29e2daa
18 changed files with 84 additions and 61 deletions

View File

@ -55,6 +55,11 @@ DATABASES = {
}
}
# Cookies
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
LANGUAGE_COOKIE_SECURE = True
# Hey Entr'ouvert is in France !!
TIME_ZONE = 'Europe/Paris'
LANGUAGE_CODE = 'fr'

View File

@ -100,12 +100,12 @@ def test_api_user(client):
Role.objects.create(name='Role4', service=service)
# test failure when unlogged
response = client.get('/api/user/', HTTP_ORIGIN='http://testserver')
response = client.get('/api/user/', HTTP_ORIGIN='https://testserver', secure=True)
assert response.content == b'{}'
# login
client.login(request=None, username='john.doe', password='password')
response = client.get('/api/user/', HTTP_ORIGIN='http://testserver')
response = client.get('/api/user/', HTTP_ORIGIN='https://testserver', secure=True)
data = json.loads(force_str(response.content))
assert isinstance(data, dict)
assert set(data.keys()) == {
@ -1055,7 +1055,7 @@ def test_register_no_email_validation(settings, app, admin, django_user_model):
assert response.json['user']['last_name'] == last_name
assert check_password(password, response.json['user']['password'])
assert response.json['token']
assert response.json['validation_url'].startswith('http://testserver/accounts/activate/')
assert response.json['validation_url'].startswith('https://testserver/accounts/activate/')
assert User.objects.count() == 2
user = User.objects.latest('id')
assert user.ou == get_default_ou()
@ -1114,7 +1114,7 @@ def test_register_ou_no_email_validation(settings, app, admin, django_user_model
assert response.json['user']['last_name'] == last_name
assert check_password(password, response.json['user']['password'])
assert response.json['token']
assert response.json['validation_url'].startswith('http://testserver/accounts/activate/')
assert response.json['validation_url'].startswith('https://testserver/accounts/activate/')
assert User.objects.count() == 2
user = User.objects.latest('id')
assert user.username == username
@ -1232,7 +1232,7 @@ def test_password_reset(app, ou1, admin, user_ou1, mailoutbox):
assert len(mailoutbox) == 1
mail = mailoutbox[0]
assert mail.to[0] == email
assert 'http://testserver/password/reset/confirm/' in mail.body
assert 'https://testserver/password/reset/confirm/' in mail.body
assert_event('manager.user.password.reset.request', user=admin, api=True)
@ -1265,7 +1265,7 @@ def test_users_email(app, ou1, admin, user_ou1, mailoutbox):
mail = mailoutbox[0]
assert mail.to[0] == new_email
assert 'http://testserver/accounts/change-email/verify/' in mail.body
assert 'https://testserver/accounts/change-email/verify/' in mail.body
def test_api_delete_role(app, admin_ou1, role_ou1):
@ -2391,7 +2391,7 @@ def test_api_statistics_list(app, admin):
assert len(resp.json['data']) == 6
login_stats = {
'name': 'Login count by authentication type',
'url': 'http://testserver/api/statistics/login/',
'url': 'https://testserver/api/statistics/login/',
'id': 'login',
'filters': [
{
@ -2411,7 +2411,7 @@ def test_api_statistics_list(app, admin):
assert login_stats in resp.json['data']
assert {
'name': 'Login count by service',
'url': 'http://testserver/api/statistics/service_login/',
'url': 'https://testserver/api/statistics/service_login/',
'id': 'service-login',
'filters': [
{

View File

@ -80,7 +80,7 @@ class FranceConnectMock:
@property
def callback_url(self):
return 'http://testserver' + reverse('fc-login-or-link')
return 'https://testserver' + reverse('fc-login-or-link')
def login_with_fc_fixed_params(self, app):
if app.session:
@ -154,7 +154,7 @@ class FranceConnectMock:
assert url.startswith('https://fcp.integ01.dev-franceconnect.fr/api/v1/logout')
parsed_url = urllib.parse.urlparse(url)
query = QueryDict(parsed_url.query)
assert_equals_url(query['post_logout_redirect_uri'], 'http://testserver' + reverse('fc-logout'))
assert_equals_url(query['post_logout_redirect_uri'], 'https://testserver' + reverse('fc-logout'))
assert query['state']
self.state = query['state']
return app.get(reverse('fc-logout') + '?state=' + self.state)

View File

@ -128,7 +128,7 @@ def test_create(settings, app, franceconnect, hooks, service, mailoutbox):
for body in (mailoutbox[0].body, mailoutbox[0].alternatives[0][0]):
assert 'Hi AnonymousUser,' in body
assert 'You have just created an account using FranceConnect.' in body
assert 'http://testserver/login/' in body
assert 'https://testserver/login/' in body
assert user.verified_attributes.first_name == 'Ÿuñe'
assert user.verified_attributes.last_name == 'Frédérique'

View File

@ -43,9 +43,9 @@ def test_api_user_franceconnect(settings, app, admin, simple_user, franceconnect
content = response.json['franceconnect']
assert isinstance(content, dict), 'franceconnect field is not a dict'
assert content.get('linked') is True
assert content.get('link_url').startswith('http://')
assert content.get('link_url').startswith('https://')
assert content.get('link_url').endswith('/callback/')
assert content.get('unlink_url').startswith('http://')
assert content.get('unlink_url').startswith('https://')
assert content.get('unlink_url').endswith('/unlink/')
unlink_url = '/api/users/%s/fc-unlink/' % simple_user.uuid

View File

@ -63,7 +63,9 @@ def app_factory():
try:
def factory(hostname='testserver'):
return django_webtest.DjangoTestApp(extra_environ={'HTTP_HOST': hostname})
return django_webtest.DjangoTestApp(
extra_environ={'HTTP_HOST': hostname, 'wsgi.url_scheme': 'https'}
)
yield factory
finally:
@ -420,7 +422,7 @@ def assert_external_redirect(external_redirect):
else:
def check_location(response, default_return):
assert urllib.parse.urljoin('http://testserver/', default_return).endswith(response['Location'])
assert urllib.parse.urljoin('https://testserver/', default_return).endswith(response['Location'])
return check_location

View File

@ -353,7 +353,7 @@ def test_authorization_code_sso(
with open('tests/200x200.jpg', 'rb') as fd:
simple_user.attributes.cityscape_image = File(fd)
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token))
assert response.json['cityscape_image'].startswith('http://testserver/media/profile-image/')
assert response.json['cityscape_image'].startswith('https://testserver/media/profile-image/')
# check against a user without username
simple_user.username = None
@ -381,7 +381,7 @@ def test_authorization_code_sso(
src = iframes.attr('src')
assert '?' in src
src_qd = QueryDict(src.split('?', 1)[1])
assert 'iss' in src_qd and src_qd['iss'] == 'http://testserver/'
assert 'iss' in src_qd and src_qd['iss'] == 'https://testserver/'
assert 'sid' in src_qd and src_qd['sid'] == get_session_id(
mock.Mock(session=app.session), oidc_client
)

View File

@ -55,7 +55,7 @@ A2_HOOKS_PROPAGATE_EXCEPTIONS = True
TEMPLATES[0]['DIRS'].append('tests/templates') # pylint: disable=undefined-variable
TEMPLATES[0]['OPTIONS']['debug'] = True # pylint: disable=undefined-variable
SITE_BASE_URL = 'http://testserver'
SITE_BASE_URL = 'https://testserver'
A2_MAX_EMAILS_PER_IP = None
A2_MAX_EMAILS_FOR_ADDRESS = None

View File

@ -541,7 +541,7 @@ def test_profile_image(db, app, admin, mailoutbox):
response = app.get('/api/users/%s/' % john().uuid)
assert (
response.json['cityscape_image']
== 'http://testserver/media/%s' % john().attributes.cityscape_image.name
== 'https://testserver/media/%s' % john().attributes.cityscape_image.name
)
app.authorization = None

View File

@ -511,7 +511,7 @@ def test_sso(app, caplog, code, oidc_provider, oidc_provider_jwkset, hooks):
assert query['response_type'] == 'code'
assert query['client_id'] == str(oidc_provider.client_id)
assert query['scope'] == 'openid'
assert query['redirect_uri'] == 'http://testserver' + reverse('oidc-login-callback')
assert query['redirect_uri'] == 'https://testserver' + reverse('oidc-login-callback')
nonce = query['nonce']
if oidc_provider.claims_parameter_supported:

View File

@ -256,7 +256,7 @@ def test_clean_unused_account_login_url(simple_user, mailoutbox):
simple_user.save()
call_command('clean-unused-accounts')
mail = mailoutbox[0]
assert 'href="http://testserver/login/"' in mail.message().as_string()
assert 'href="https://testserver/login/"' in mail.message().as_string()
def test_clean_unused_account_with_no_email(simple_user, mailoutbox, caplog):

View File

@ -559,7 +559,7 @@ tnoel@entrouvert.com,Thomas,Noël,'''
assert importer.run()
thomas = User.objects.get(email='tnoel@entrouvert.com')
assert len(mail.outbox) == 1
assert 'http://testserver/password/reset/confirm/' in mail.outbox[0].body
assert 'https://testserver/password/reset/confirm/' in mail.outbox[0].body
password = thomas.password
del mail.outbox[0]

View File

@ -149,7 +149,7 @@ class SamlSP:
self.base_url = 'https://sp.example.com'
self.name = 'Test SP'
self.slug = 'test-sp'
self.idp_entity_idp = ('http://testserver/idp/saml2/metadata',)
self.idp_entity_idp = ('https://testserver/idp/saml2/metadata',)
self.default_name_id_format = 'email'
self.accepted_name_id_format = ['email', 'persistent', 'transient', 'username']
self.ou = OrganizationalUnit.objects.get()
@ -504,7 +504,7 @@ class Scenario:
),
(
"/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='avatar']/saml:AttributeValue",
re.compile('^http://testserver/media/profile-image/.*$'),
re.compile('^https://testserver/media/profile-image/.*$'),
),
(
"/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='verified_attributes']/@NameFormat",
@ -662,7 +662,7 @@ def add_attributes(rf):
with mock.patch(
'authentic2.idp.saml.saml2_endpoints.get_attributes', wraps=saml2_endpoints.get_attributes
) as get_attributes:
request = rf.get('/')
request = rf.get('/', secure=True)
request.user = None
assertion = lasso.Saml2Assertion()
provider = Service(ou=None)
@ -815,14 +815,16 @@ def test_make_edu_person_targeted_id(db, settings, rf):
== '_' + hashlib.sha256(b'b' + b'https://sp.com/' + b'a').hexdigest().upper()
)
edpt = saml2_endpoints.make_edu_person_targeted_id('http://testserver/idp/saml2/metadata', provider, user)
edpt = saml2_endpoints.make_edu_person_targeted_id(
'https://testserver/idp/saml2/metadata', provider, user
)
assert edpt is not None
node = lasso.Node.newFromXmlNode(force_str(ET.tostring(edpt)))
assert isinstance(node, lasso.Saml2NameID)
assert force_str(node.content) == '_A485C0ACEEF43A6D39145F5CFE25D9D3B6F15DC6443F412263C76D81C72DA8D5'
assert node.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
assert node.nameQualifier == 'http://testserver/idp/saml2/metadata'
assert node.nameQualifier == 'https://testserver/idp/saml2/metadata'
assert node.spNameQualifier == 'https://sp.com/'
@ -854,7 +856,7 @@ def test_add_attributes_edu_person_targeted_id_nid_format(db, settings, rf, add_
assert isinstance(node, lasso.Saml2NameID)
assert force_str(node.content) == '_A485C0ACEEF43A6D39145F5CFE25D9D3B6F15DC6443F412263C76D81C72DA8D5'
assert node.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
assert node.nameQualifier == 'http://testserver/idp/saml2/metadata'
assert node.nameQualifier == 'https://testserver/idp/saml2/metadata'
assert node.spNameQualifier == 'https://sp.com/'
@ -886,7 +888,7 @@ def test_add_attributes_edu_person_targeted_id_attribute(db, settings, rf, add_a
assert isinstance(node, lasso.Saml2NameID)
assert force_str(node.content) == '_A485C0ACEEF43A6D39145F5CFE25D9D3B6F15DC6443F412263C76D81C72DA8D5'
assert node.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
assert node.nameQualifier == 'http://testserver/idp/saml2/metadata'
assert node.nameQualifier == 'https://testserver/idp/saml2/metadata'
assert node.spNameQualifier == 'https://sp.com/'

View File

@ -1034,7 +1034,7 @@ def test_manager_role_admin_permissions(app, simple_user, admin, simple_role):
assert q('table tbody tr td .icon-remove-sign')
token = str(response.context['csrf_token'])
params = {'action': 'remove', 'user_or_role': 'user-%s' % admin.pk, 'csrfmiddlewaretoken': token}
app.post('/manage/roles/%s/' % simple_role.pk, params=params)
app.post('/manage/roles/%s/' % simple_role.pk, params=params, headers={'Referer': 'https://testserver/'})
assert simple_role not in admin.roles.all()
# user can act on role inheritance
@ -1049,21 +1049,33 @@ def test_manager_role_admin_permissions(app, simple_user, admin, simple_role):
response = app.get('/manage/roles/%s/children/' % simple_role.pk)
token = str(response.context['csrf_token'])
params = {'action': 'add', 'role': role.pk, 'csrfmiddlewaretoken': token}
response = app.post('/manage/roles/%s/children/' % simple_role.pk, params=params)
response = app.post(
'/manage/roles/%s/children/' % simple_role.pk,
params=params,
headers={'Referer': 'https://testserver/'},
)
assert role in simple_role.children()
params = {'action': 'remove', 'role': role.pk, 'csrfmiddlewaretoken': token}
response = app.post('/manage/roles/%s/children/' % simple_role.pk, params=params)
response = app.post(
'/manage/roles/%s/children/' % simple_role.pk,
params=params,
headers={'Referer': 'https://testserver/'},
)
assert role not in simple_role.children()
response = app.get('/manage/roles/%s/parents/' % role.pk)
token = str(response.context['csrf_token'])
params = {'action': 'add', 'role': simple_role.pk, 'csrfmiddlewaretoken': token}
response = app.post('/manage/roles/%s/parents/' % role.pk, params=params)
response = app.post(
'/manage/roles/%s/parents/' % role.pk, params=params, headers={'Referer': 'https://testserver/'}
)
assert simple_role in role.parents()
params = {'action': 'remove', 'role': simple_role.pk, 'csrfmiddlewaretoken': token}
response = app.post('/manage/roles/%s/parents/' % role.pk, params=params)
response = app.post(
'/manage/roles/%s/parents/' % role.pk, params=params, headers={'Referer': 'https://testserver/'}
)
assert simple_role not in role.parents()
# user can add role as a member through role members form
@ -1078,7 +1090,7 @@ def test_manager_role_admin_permissions(app, simple_user, admin, simple_role):
assert q('table tbody tr td .icon-remove-sign')
token = str(response.context['csrf_token'])
params = {'action': 'remove', 'user_or_role': 'role-%s' % role.pk, 'csrfmiddlewaretoken': token}
app.post('/manage/roles/%s/' % simple_role.pk, params=params)
app.post('/manage/roles/%s/' % simple_role.pk, params=params, headers={'Referer': 'https://testserver/'})
assert role not in simple_role.children()
# try to add arbitrary role
@ -1086,7 +1098,11 @@ def test_manager_role_admin_permissions(app, simple_user, admin, simple_role):
response = app.get('/manage/roles/%s/parents/' % role.pk)
token = str(response.context['csrf_token'])
params = {'action': 'add', 'role': admin_role.pk, 'csrfmiddlewaretoken': token}
response = app.post('/manage/roles/%s/parents/' % simple_role.pk, params=params)
response = app.post(
'/manage/roles/%s/parents/' % simple_role.pk,
params=params,
headers={'Referer': 'https://testserver/'},
)
assert admin_role not in role.parents()
# user roles view works
@ -1097,7 +1113,9 @@ def test_manager_role_admin_permissions(app, simple_user, admin, simple_role):
token = str(response.context['csrf_token'])
params = {'action': 'add', 'role': simple_role.pk, 'csrfmiddlewaretoken': token}
response = app.post('/manage/users/%s/roles/' % admin.pk, params=params)
response = app.post(
'/manage/users/%s/roles/' % admin.pk, params=params, headers={'Referer': 'https://testserver/'}
)
assert simple_role in admin.roles.all()
app.get('/manage/roles/add/', status=403)
@ -1306,11 +1324,11 @@ def test_manager_menu_json(app, admin):
{
'label': 'Identity management',
'slug': 'identity-management',
'url': 'http://testserver/manage/',
'url': 'https://testserver/manage/',
'sub': False,
},
{'label': 'Users', 'slug': 'users', 'url': 'http://testserver/manage/users/', 'sub': True},
{'label': 'Roles', 'slug': 'roles', 'url': 'http://testserver/manage/roles/', 'sub': True},
{'label': 'Users', 'slug': 'users', 'url': 'https://testserver/manage/users/', 'sub': True},
{'label': 'Roles', 'slug': 'roles', 'url': 'https://testserver/manage/roles/', 'sub': True},
]
response = login(app, admin)

View File

@ -176,11 +176,11 @@ def test_send_password_reset_email_no_account(app, db, mailoutbox, settings, reg
for body in (mail.body, mail.alternatives[0][0]):
assert 'no account was found associated with this address' in body
if settings.REGISTRATION_OPEN:
assert 'http://testserver/register/' in body
assert 'https://testserver/register/' in body
# check next_url was preserved
assert 'next=/whatever/' in body
else:
assert 'http://testserver/register/' not in body
assert 'https://testserver/register/' not in body
def test_send_password_reset_email_disabled_account(app, simple_user, mailoutbox):
@ -209,14 +209,9 @@ def test_honeypot(app, db, settings, mailoutbox):
url = reverse('password_reset')
response = app.get(url, status=200)
response = app.post(
url,
params={
'email': 'testbot@entrouvert.com',
'csrfmiddlewaretoken': response.context['csrf_token'],
'robotcheck': 'a',
},
)
response.form.set('email', 'testbot@entrouvert.com')
response.form.set('robotcheck', True)
response = response.form.submit()
response = response.follow()
assert len(mailoutbox) == 0
assert 'Your password reset request has been refused' in response

View File

@ -812,14 +812,9 @@ def test_honeypot(app, db, settings, mailoutbox):
settings.DEFAULT_FROM_EMAIL = 'show only addr <noreply@example.net>'
response = app.get(utils_misc.make_url('registration_register'))
response = app.post(
utils_misc.make_url('registration_register'),
params={
'email': 'testbot@entrouvert.com',
'csrfmiddlewaretoken': response.context['csrf_token'],
'robotcheck': 'a',
},
)
response.form.set('email', 'testbot@entrouvert.com')
response.form.set('robotcheck', True)
response = response.form.submit()
response = response.follow()
assert len(mailoutbox) == 0
assert 'Your registration request has been refused' in response

View File

@ -622,13 +622,17 @@ def test_role_members_user_role_add_remove(app, superuser, settings, simple_role
# simulate click on Jôhn Dôe delete icon
token = str(resp.context['csrf_token'])
params = {'action': 'remove', 'user_or_role': 'user-%s' % simple_user.pk, 'csrfmiddlewaretoken': token}
resp = app.post('/manage/roles/%s/' % simple_role.pk, params=params).follow()
resp = app.post(
'/manage/roles/%s/' % simple_role.pk, params=params, headers={'Referer': 'https://testserver/'}
).follow()
assert 'Jôhn Dôe' not in resp.text
# simulate click on role_ou1 delete icon
token = str(resp.context['csrf_token'])
params = {'action': 'remove', 'user_or_role': 'role-%s' % role_ou1.pk, 'csrfmiddlewaretoken': token}
resp = app.post('/manage/roles/%s/' % simple_role.pk, params=params).follow()
resp = app.post(
'/manage/roles/%s/' % simple_role.pk, params=params, headers={'Referer': 'https://testserver/'}
).follow()
assert 'role_ou1' not in resp.text
# invalid choices are ignored

View File

@ -1174,7 +1174,9 @@ def test_manager_user_authorizations(app, superuser, simple_user):
# cannot click it's JS :/
token = str(resp.context['csrf_token'])
params = {'authorization': auth.pk, 'csrfmiddlewaretoken': token}
resp = app.post(user_authorizations_url, params=params, status=302)
resp = app.post(
user_authorizations_url, params=params, status=302, headers={'Referer': 'https://testserver/'}
)
assert OIDCAuthorization.objects.count() == 0
resp = resp.follow()
assert resp.html.find('td').text == 'This user has not granted profile data access to any service yet.'