Skip to content

Commit

Permalink
Merge pull request #259 from GermanZero-de/23-user-access-structure
Browse files Browse the repository at this point in the history
City Editors and Admins and their permissions #23
  • Loading branch information
fblampe authored Aug 25, 2023
2 parents 741f9a1 + f4a4ef8 commit 0f57950
Show file tree
Hide file tree
Showing 18 changed files with 3,191 additions and 435 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,17 @@ pytest --headed <path-to-e2e-test>
From a local database filled with suitable data, generate a fixture named `example_fixture` with

```shell
python -Xutf8 manage.py dumpdata cpmonitor -e contenttypes -e admin.logentry -e sessions --indent 2 --settings=config.settings.local > cpmonitor/fixtures/example_fixture.json
python -Xutf8 manage.py dumpdata -e contenttypes -e auth.Permission -e admin.LogEntry -e sessions --indent 2 --settings=config.settings.local > cpmonitor/fixtures/example_fixture.json
```

(The `-Xutf8` and `--indent 2` options ensure consistent and readable output on all platforms.)

The arguments `-e contenttypes -e auth.Permission -e admin.LogEntry -e sessions` exclude tables which are pre-filled
by django or during usage by django and whose content may change depending on the models in the project. If they are
included, everything works fine at first, since loaddata will silently accept data already there. However, as soon as
the data to load clashes with existing content, it will fail. `-e admin.LogEntry` excludes references to content types
which may otherwise be inconsistent.`-e sessions` excludes unneeded data which otherwise would clog the JSON file.

This fixture may be loaded in a test with. (Similar in a pytest fixture.)

```python
Expand Down Expand Up @@ -241,7 +247,7 @@ Afterwards the test database has to be updated as well. Use the dumpdata command
currently running database:

```shell
python -Xutf8 manage.py dumpdata -e contenttypes -e admin.logentry -e sessions --indent 2 --settings=config.settings.local > e2e_tests/database/test_database.json
python -Xutf8 manage.py dumpdata -e contenttypes -e auth.Permission -e admin.LogEntry -e sessions --indent 2 --settings=config.settings.local > e2e_tests/database/test_database.json
```

Cheat-sheet to make sure the correct data is dumped:
Expand All @@ -254,7 +260,7 @@ python manage.py loaddata --settings=config.settings.local e2e_tests/database/te
cp -r e2e_tests/database/test_database_uploads/. cpmonitor/images/uploads
git checkout after-model-change-including-migration
python manage.py migrate --settings=config.settings.local
python -Xutf8 manage.py dumpdata -e contenttypes -e admin.logentry -e sessions --indent 2 --settings=config.settings.local > e2e_tests/database/test_database.json
python -Xutf8 manage.py dumpdata -e contenttypes -e auth.Permission -e admin.LogEntry -e sessions --indent 2 --settings=config.settings.local > e2e_tests/database/test_database.json
# Only if additional images were uploaded:
cp -r cpmonitor/images/uploads e2e_tests/database/test_database_uploads
```
Expand Down Expand Up @@ -371,7 +377,7 @@ Possibly migrate, test the data, and check that the size is reasonable. Then mak

```sh
SNAPSHOT_NAME=prod_database_$(date -u +"%FT%H%M%SZ")
python -Xutf8 manage.py dumpdata -e contenttypes -e admin.logentry -e sessions --indent 2 --settings=config.settings.local > e2e_tests/database/${SNAPSHOT_NAME}.json
python -Xutf8 manage.py dumpdata -e contenttypes -e auth.Permission -e admin.LogEntry -e sessions --indent 2 --settings=config.settings.local > e2e_tests/database/${SNAPSHOT_NAME}.json
cp -r cpmonitor/images/uploads e2e_tests/database/${SNAPSHOT_NAME}_uploads
echo "Some useful information, e.g. the migration state of the snapshot" > e2e_tests/database/${SNAPSHOT_NAME}.README
du -hs e2e_tests/database/${SNAPSHOT_NAME}*
Expand Down
43 changes: 42 additions & 1 deletion config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ def get_env(var: str) -> str:
"django.contrib.staticfiles",
"treebeard",
"martor",
"rules.apps.AutodiscoverRulesConfig",
"allauth",
"allauth.account",
"allauth.socialaccount",
# "invitations", We do not use invitations.Invitation and therefore do not want its migrations.
"cpmonitor.apps.CpmonitorConfig",
]

Expand All @@ -60,12 +65,17 @@ def get_env(var: str) -> str:
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

AUTHENTICATION_BACKENDS = (
"rules.permissions.ObjectPermissionBackend",
"django.contrib.auth.backends.ModelBackend",
)

ROOT_URLCONF = "cpmonitor.urls"

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"DIRS": [BASE_DIR / "cpmonitor" / "templates" / "overrides"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
Expand Down Expand Up @@ -157,3 +167,34 @@ def get_env(var: str) -> str:
MARTOR_UPLOAD_PATH = "uploads/"
MARTOR_UPLOAD_URL = "/api/uploader/"
MAX_IMAGE_UPLOAD_SIZE = 104857600 # 100 MB

# django-allauth configuration:
# https://django-allauth.readthedocs.io/en/latest/configuration.html
# Most customization is done in the adapter:
ACCOUNT_ADAPTER = "cpmonitor.adapters.AllauthInvitationsAdapter"
# django-allauth needs allauth.socialaccount to really work, but we don't use its OAuth parts
SOCIALACCOUNT_PROVIDERS = {}
ACCOUNT_EMAIL_VERIFICATION = "none" # Would need a working email config.

# django-invitations configuration:
# https://django-invitations.readthedocs.io/en/latest/configuration.html
# django-invitations is closely coupled to django-allauth and uses
# the same adapter.
INVITATIONS_ADAPTER = "cpmonitor.adapters.AllauthInvitationsAdapter"
# To couple an invitation to a city and access right (either admin or editor)
# a custom model is needed. To prevent `invitations.Invitation` from being
# used, django-invitations does not appear in `INSTALLED_APPS` above.
INVITATIONS_INVITATION_MODEL = "cpmonitor.Invitation"
INVITATIONS_GONE_ON_ACCEPT_ERROR = False
INVITATIONS_INVITATION_ONLY = True
# In order to use our custom view instead of the one from django-invitations,
# `invitations.urls` could not be used. This parameters default value points
# to that and had to be replaced by our own:
INVITATIONS_CONFIRMATION_URL_NAME = "accept-invite"
# Setting this to true would cause a signal handler to be installed, which
# would try to access the email field of an invitation and fail on the custom model.
INVITATIONS_ACCEPT_INVITE_AFTER_SIGNUP = False

# django core configuration used by django-invitations, django-allauth
LOGIN_URL = "/admin/login/"
LOGIN_REDIRECT_URL = "/admin/"
46 changes: 46 additions & 0 deletions cpmonitor/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from allauth.account.adapter import DefaultAccountAdapter
from invitations.app_settings import app_settings

from .models import AccessRight, City, get_invitation


class AllauthInvitationsAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
"""
Overwrites django-invitations.
Checks that there exists an invitation instead of email.
"""
if get_invitation(request):
return True
elif app_settings.INVITATION_ONLY:
return False
else:
return True

def save_user(self, request, user, form, commit=True):
"""
Overwrites django-allauth.
Check there is an invitation and set the appropriate access rights.
Swallow the user object already created, if not.
Otherwise, set access rights according to invitation.
"""
invitation = get_invitation(request)
if not invitation:
self.add_error(
None,
"Die Registrierung ist nur möglich über einen gültigen Einladungslink.",
)
return

user.is_staff = True
user = super().save_user(request, user, form, commit)

city: City = invitation.city
if invitation.access_right == AccessRight.CITY_EDITOR:
city.city_editors.add(user)
city.save()
elif invitation.access_right == AccessRight.CITY_ADMIN:
city.city_admins.add(user)
city.save()

return user
Loading

0 comments on commit 0f57950

Please sign in to comment.