Skip to content

Commit

Permalink
Hearing auth method restrictions (#69)
Browse files Browse the repository at this point in the history
Changes:
- added a model and endpoint for authentication methods
- added ability to restrict hearing visibility by given authentication methods
  • Loading branch information
SanttuA authored Mar 17, 2023
1 parent 89c85ad commit 2696d75
Show file tree
Hide file tree
Showing 15 changed files with 534 additions and 12 deletions.
10 changes: 9 additions & 1 deletion democracy/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ class Media:
"fields": ("project_phase",)
}),
(_("Availability"), {
"fields": ("published", "open_at", "close_at", "force_closed")
"fields": (
"published", "open_at", "close_at", "force_closed","visible_for_auth_methods"
)
}),
(_("Area"), {
"fields": ("geometry",)
Expand Down Expand Up @@ -452,6 +454,11 @@ def get_any_language(obj, attr_name):
return translation


class AuthMethodAdmin(admin.ModelAdmin):
exclude = ('published',)
readonly_fields = ('id',)


# Wire it up!


Expand All @@ -463,3 +470,4 @@ def get_any_language(obj, attr_name):
admin.site.register(models.Project, ProjectAdmin)
admin.site.register(models.ProjectPhase, ProjectPhaseAdmin)
admin.site.register(models.SectionComment, CommentAdmin)
admin.site.register(models.AuthMethod, AuthMethodAdmin)
15 changes: 15 additions & 0 deletions democracy/locale/fi/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,18 @@ msgstr "Lisätty: %(email)s"

msgid "Removed: %(email)s"
msgstr "Poistettu: %(email)s"

msgid "Authentication method"
msgstr "Tunnistustapa"

msgid "Authentication methods"
msgstr "Tunnistustavat"

msgid "id of the authentication method"
msgstr "Tunnistustavan amr-tunniste"

msgid "Visible for authentication methods"
msgstr "Näkyvissä näille tunnistustavoille"

msgid "Only users who use given authentication methods are allowed to see this hearing"
msgstr "Vain käyttäjät, jotka ovat tunnistautuneet annetuilla tunnistustavoilla, voivat nähdä tämän kuulemisen"
43 changes: 43 additions & 0 deletions democracy/migrations/0060_hearing_auth_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 2.2.28 on 2023-03-10 08:34

from django.conf import settings
import django.core.files.storage
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('democracy', '0059_add_organization_log'),
]

operations = [
migrations.CreateModel(
name='AuthMethod',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now, editable=False, verbose_name='time of creation')),
('modified_at', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='time of last modification')),
('published', models.BooleanField(db_index=True, default=True, verbose_name='public')),
('deleted_at', models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion')),
('deleted', models.BooleanField(db_index=True, default=False, editable=False, verbose_name='deleted')),
('name', models.CharField(default='', max_length=200, verbose_name='name')),
('amr', models.CharField(help_text='id of the authentication method', max_length=100, unique=True)),
('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='authmethod_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('deleted_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='authmethod_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by')),
('modified_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='authmethod_modified', to=settings.AUTH_USER_MODEL, verbose_name='last modified by')),
],
options={
'verbose_name': 'Authentication method',
'verbose_name_plural': 'Authentication methods',
},
),
migrations.AddField(
model_name='hearing',
name='visible_for_auth_methods',
field=models.ManyToManyField(blank=True, help_text='Only users who use given authentication methods are allowed to see this hearing', related_name='hearings', to='democracy.AuthMethod', verbose_name='Visible for authentication methods'),
),
]
2 changes: 2 additions & 0 deletions democracy/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .section import SectionPoll, SectionPollOption, SectionPollAnswer
from .organization import ContactPerson, Organization, OrganizationLog
from .project import Project, ProjectPhase
from .auth_method import AuthMethod

__all__ = [
"ContactPerson",
Expand All @@ -21,4 +22,5 @@
"Project",
"ProjectPhase",
"SectionFile",
"AuthMethod",
]
21 changes: 21 additions & 0 deletions democracy/models/auth_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _

from .base import BaseModel


class AuthMethod(BaseModel):
'''Model representing a single authentication method in an authentication service'''
name = models.CharField(verbose_name=_('name'), default='', max_length=200)
amr = models.CharField(
help_text=_('id of the authentication method'),
max_length=100,
unique=True
)

class Meta:
verbose_name = _('Authentication method')
verbose_name_plural = _('Authentication methods')

def __str__(self):
return f'{self.name} ({self.amr})'
20 changes: 18 additions & 2 deletions democracy/models/hearing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .base import BaseModelManager, StringIdBaseModel
from .organization import ContactPerson, Organization
from .project import ProjectPhase
from .auth_method import AuthMethod


class HearingQueryset(TranslatableQuerySet):
Expand Down Expand Up @@ -59,6 +60,13 @@ class Hearing(StringIdBaseModel, TranslatableModel):
contact_persons = models.ManyToManyField(ContactPerson, verbose_name=_('contact persons'), related_name='hearings')
project_phase = models.ForeignKey(ProjectPhase, verbose_name=_('project phase'), related_name='hearings',
on_delete=models.PROTECT, null=True, blank=True)
visible_for_auth_methods = models.ManyToManyField(
AuthMethod,
verbose_name=_('Visible for authentication methods'),
help_text=_('Only users who use given authentication methods are allowed to see this hearing'),
related_name='hearings',
blank=True
)

objects = BaseModelManager.from_queryset(HearingQueryset)()
original_manager = models.Manager()
Expand Down Expand Up @@ -118,9 +126,17 @@ def get_main_section(self):
except ObjectDoesNotExist:
return None

def is_visible_for(self, user):
def is_visible_for(self, user, amr=None):
if self.published and self.open_at < now():
return True
visible_for_auth_methods = self.visible_for_auth_methods.all()
if visible_for_auth_methods:
if not user.is_authenticated:
return False
for auth_method in visible_for_auth_methods:
if auth_method.amr == amr:
return True
else:
return True
if not user.is_authenticated:
return False
if user.is_superuser:
Expand Down
65 changes: 64 additions & 1 deletion democracy/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

from democracy.enums import Commenting, InitialSectionType, CommentingMapTools
from democracy.factories.hearing import HearingFactory, LabelFactory
from democracy.models import ContactPerson, Hearing, Label, Project, ProjectPhase, Section, SectionFile, SectionType, Organization
from democracy.models import (
ContactPerson, Hearing, Label, Project, ProjectPhase, Section, SectionFile, SectionType,
Organization, AuthMethod
)
from democracy.tests.utils import FILES, assert_ascending_sequence, create_default_images, create_default_files, get_file_path


Expand Down Expand Up @@ -64,6 +67,28 @@ def contact_person(default_organization):
)


@pytest.fixture()
def auth_method_library_card():
"""
Fixture for a single authentication method "Library card" in some authentication service
"""
return AuthMethod.objects.create(
name='Library Card',
amr='lib_card'
)


@pytest.fixture()
def auth_method_test_auth():
"""
Fixture for a single authentication method "Test auth" in some authentication service
"""
return AuthMethod.objects.create(
name='Test Auth',
amr='test_amr'
)


@pytest.fixture()
def default_hearing(john_doe, contact_person, default_organization, default_project):
"""
Expand Down Expand Up @@ -167,6 +192,44 @@ def hearing_without_comments(contact_person, default_organization, default_proje

return hearing

@pytest.fixture()
def hearing_with_auth_method_restriction(
contact_person, default_organization, default_project, auth_method_library_card, john_doe
):
"""
Fixture for a hearing with multiple sections and its visibility is restricted by
an auth method. Commenting is open.
"""
hearing = Hearing.objects.create(
title='Hearing with auth method restriction',
open_at=now() - datetime.timedelta(days=1),
close_at=now() + datetime.timedelta(days=1),
slug='auth-method-hearing-slug',
organization=default_organization,
project_phase=default_project.phases.all()[0],
)

for x in range(1, 4):
section_type = (InitialSectionType.MAIN if x == 1 else InitialSectionType.SCENARIO)
section = Section.objects.create(
abstract='Section %d abstract' % x,
hearing=hearing,
type=SectionType.objects.get(identifier=section_type),
commenting=Commenting.OPEN,
commenting_map_tools=CommentingMapTools.ALL
)
create_default_images(section)
create_default_files(section)
section.comments.create(created_by=john_doe, content=default_comment_content[::-1])
section.comments.create(created_by=john_doe, content=red_comment_content[::-1])
section.comments.create(created_by=john_doe, content=green_comment_content[::-1])

assert_ascending_sequence([s.ordering for s in hearing.sections.all()])
hearing.contact_persons.add(contact_person)
hearing.visible_for_auth_methods.add(auth_method_library_card)

return hearing


@pytest.fixture()
def strong_auth_hearing(john_doe, contact_person, default_organization, default_project):
Expand Down
90 changes: 90 additions & 0 deletions democracy/tests/test_auth_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import pytest

from democracy.tests.utils import get_data_from_response


list_url = '/v1/auth_method/'

def get_detail_url(auth_method):
return list_url + str(auth_method.pk) + '/'


@pytest.mark.django_db
@pytest.mark.parametrize('client', [
'api_client',
'jane_doe_api_client',
'admin_api_client'
])
def test_get_auth_methods_list(client, request, auth_method_library_card, auth_method_test_auth):
"""
Tests that auth methods can be fetched via list endpoint by anyone
"""
api_client = request.getfixturevalue(client)
response = api_client.get(list_url)
data = get_data_from_response(response)
ids = [auth_method['id'] for auth_method in data['results']]
assert auth_method_library_card.id in ids
assert auth_method_test_auth.id in ids


@pytest.mark.django_db
@pytest.mark.parametrize('client, expected', [
('api_client', 401),
('jane_doe_api_client', 403),
('admin_api_client', 405)
])
def test_post_auth_methods_list(client, expected, request):
"""
Tests that auth methods cannot be created via list endpoint by anyone
"""
api_client = request.getfixturevalue(client)
data = {'name': 'test', 'amr': 'some_amr'}
response = api_client.post(list_url, data)
assert response.status_code == expected


@pytest.mark.django_db
@pytest.mark.parametrize('client', [
'api_client',
'jane_doe_api_client',
'admin_api_client'
])
def test_get_auth_method_detail(client, request, auth_method_library_card):
"""
Tests that an auth method can be fetched via detail endpoint by anyone
"""
api_client = request.getfixturevalue(client)
response = api_client.get(get_detail_url(auth_method_library_card))
data = get_data_from_response(response)
assert auth_method_library_card.id == data.get('id')


@pytest.mark.django_db
@pytest.mark.parametrize('client, expected', [
('api_client', 401),
('jane_doe_api_client', 403),
('admin_api_client', 405)
])
def test_update_auth_method_detail(client, expected, request, auth_method_library_card):
"""
Tests that auth methods cannot be updated via detail endpoint by anyone
"""
api_client = request.getfixturevalue(client)
data = {'name': 'test', 'amr': 'some_amr'}
response = api_client.put(get_detail_url(auth_method_library_card), data)
assert response.status_code == expected


@pytest.mark.django_db
@pytest.mark.parametrize('client, expected', [
('api_client', 401),
('jane_doe_api_client', 403),
('admin_api_client', 405)
])
def test_delete_auth_method_detail(client, expected, request, auth_method_library_card):
"""
Tests that auth methods cannot be deleted via detail endpoint by anyone
"""
api_client = request.getfixturevalue(client)
response = api_client.delete(get_detail_url(auth_method_library_card))
assert response.status_code == expected
Loading

0 comments on commit 2696d75

Please sign in to comment.