Skip to content

Commit bb70276

Browse files
committed
Merge branch 'master' into release
2 parents 378a090 + 5cf0f71 commit bb70276

File tree

92 files changed

+715
-239
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+715
-239
lines changed

.env.example

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ LETSENCRYPT_RSA_KEY_SIZE=4096
3838
# Set to 1 if you're testing your setup to avoid hitting request limits
3939
LETSENCRYPT_STAGING=1
4040

41+
####################
42+
# Storage settings
43+
####################
4144

4245
# Used to define storages in QFieldCloud. Note the contents of this variable is a superset of Django's `STORAGES` setting.
43-
# NOTE: Note if the DYNAMIC_STORAGES is not available, QFieldCloud will still work with `STORAGE_ACCESS_KEY_ID`, `STORAGE_SECRET_KEY_ID`, `STORAGE_BUCKET_NAME` and `STORAGE_REGION_NAME` from previous QFC versions.
46+
# NOTE: Note if the `STORAGES` is not available, QFieldCloud will still work with `STORAGE_ACCESS_KEY_ID`, `STORAGE_SECRET_KEY_ID`, `STORAGE_BUCKET_NAME` and `STORAGE_REGION_NAME` from previous QFC versions.
4447
# NOTE: The custom property `QFC_IS_LEGACY` is temporary available to allow migration from the old to the new way of handling files. This option will soon be removed, so you are highly encouraged to migrate all the projects to the new way of handling files.
4548
# NOTE: The `endpoint_url` must be a URL reachable from within docker and the host, the default value `172.17.0.1` for `minio` is the docker network `bridge`. On windows/mac, change the value to "http://host.docker.internal:8009".
4649
# DEFAULT:
@@ -76,6 +79,16 @@ STORAGES='{
7679
# DEFAULT: ""
7780
# STORAGES_PROJECT_DEFAULT_STORAGE=
7881

82+
# Local admin username configuration for minio storage in local and standalone instances. Uncomment if necessary.
83+
# NOTE: Needs to be the same as in the `STORAGES` setting in standalone config.
84+
# DEFAULT: MINIO_ROOT_USER=minioadmin
85+
# MINIO_ROOT_USER=minioadmin
86+
87+
# Local admin password configuration for minio storage in local and standalone instances. Uncomment if necessary.
88+
# NOTE: Needs to be the same as in the `STORAGES` setting in standalone config.
89+
# DEFAULT: MINIO_ROOT_PASSWORD=minioadmin
90+
# MINIO_ROOT_PASSWORD=minioadmin
91+
7992
# Public port to the minio API endpoint. It must match the configured port in `STORAGE_ENDPOINT_URL`.
8093
# NOTE: active only when minio is the configured as storage endpoint. Mostly for local development.
8194
# DEFAULT: 8009

docker-app/qfieldcloud/authentication/authentication.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
from django.http.request import HttpRequest
44
from django.utils import timezone
55
from django.utils.translation import gettext as _
6-
from qfieldcloud.core.models import User
76
from rest_framework.authentication import (
87
TokenAuthentication as DjangoRestFrameworkTokenAuthentication,
98
)
109

10+
from qfieldcloud.core.models import User
11+
1112
from ..core.exceptions import AuthenticationViaTokenFailedError
1213
from .models import AuthToken
1314

docker-app/qfieldcloud/authentication/migrations/0001_initial.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Generated by Django 3.2.7 on 2021-10-01 22:01
22

33
import django.db.models.deletion
4-
import qfieldcloud.authentication.models
54
from django.conf import settings
65
from django.db import migrations, models
76

7+
import qfieldcloud.authentication.models
8+
89

910
class Migration(migrations.Migration):
1011
initial = True

docker-app/qfieldcloud/authentication/tests/test_authentication.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
from django.core.files.base import ContentFile
55
from django.utils.timezone import now
6+
from rest_framework.test import APITransactionTestCase
7+
68
from qfieldcloud.authentication.models import AuthToken
79
from qfieldcloud.core.models import Organization, Person, Team
810
from qfieldcloud.core.tests.utils import setup_subscription_plans
9-
from rest_framework.test import APITransactionTestCase
1011

1112
logging.disable(logging.CRITICAL)
1213

docker-app/qfieldcloud/core/adapters.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
import logging
2+
import traceback
3+
from random import randint
4+
15
from allauth.account import app_settings
26
from allauth.account.adapter import DefaultAccountAdapter
7+
from allauth.account.models import EmailConfirmationHMAC
8+
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
9+
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
10+
from django.contrib.auth.models import AbstractUser
311
from django.core.exceptions import ValidationError
12+
from django.http import HttpRequest
413
from invitations.adapters import BaseInvitationsAdapter
14+
515
from qfieldcloud.core.models import Person
6-
from allauth.account.models import EmailConfirmationHMAC
7-
from django.http import HttpRequest
16+
17+
logger = logging.getLogger(__name__)
818

919

1020
class AccountAdapter(DefaultAccountAdapter, BaseInvitationsAdapter):
@@ -53,6 +63,43 @@ def clean_username(self, username, shallow=False):
5363

5464
return result
5565

66+
def populate_username(self, request: HttpRequest, user: AbstractUser) -> None:
67+
"""Customize username population for signups via social logins.
68+
69+
When a user signs up via username and password, we try to respect their
70+
choice of username, and just delegate to the default implementation to
71+
avoid collisions.
72+
73+
For users that directly sign up via a social login however, we:
74+
- Take the local part of their email (part before the '@' sign)
75+
- Append a random 4-digit suffix to make it likely to be unique and
76+
not communicate any information about the existence of other users
77+
- Let generate_unique_username() normalize the username and ensure its
78+
uniqueness.
79+
"""
80+
81+
from allauth.account.utils import user_email, user_username
82+
83+
email = user_email(user)
84+
username = user_username(user)
85+
86+
if username:
87+
# Manually chosen username - defer to default implementation
88+
return super().populate_username(request, user)
89+
90+
# Signup via social login - automatically generate a unique username
91+
localpart = email.split("@")[0]
92+
suffix = str(randint(1000, 9999))
93+
username_candidate = f"{localpart}{suffix}"
94+
95+
if app_settings.USER_MODEL_USERNAME_FIELD:
96+
user_username(
97+
user,
98+
self.generate_unique_username(
99+
[username_candidate], regex=r"[^\w\s\-_]"
100+
),
101+
)
102+
56103
def send_confirmation_mail(
57104
self,
58105
request: HttpRequest,
@@ -69,3 +116,37 @@ def send_confirmation_mail(
69116
)
70117

71118
super().send_confirmation_mail(request, email_confirmation, signup)
119+
120+
121+
class SocialAccountAdapter(DefaultSocialAccountAdapter):
122+
"""Custom SocialAccountAdapter to aid SSO integration in QFC.
123+
124+
Logs stack trace and error details on 3rd party authentication errors.
125+
"""
126+
127+
def on_authentication_error(
128+
self,
129+
request: HttpRequest,
130+
provider: OAuth2Provider,
131+
error: str = None,
132+
exception: Exception = None,
133+
extra_context: dict = None,
134+
) -> None:
135+
logger.error("SSO Authentication error:", exc_info=True)
136+
logger.error(f"Provider: {provider!r}")
137+
logger.error(f"Error: {error!r}")
138+
139+
if not extra_context:
140+
extra_context = {}
141+
142+
# Make stack strace available in template context.
143+
#
144+
# That way, it could be displayed in the frontend (for debugging
145+
# purposes), by overriding socialaccount/authentication_error.html and
146+
# using {{ formatted_exception }} to display the stack trace.
147+
extra_context["formatted_exception"] = "\n".join(
148+
traceback.format_exception(exception)
149+
)
150+
super().on_authentication_error(
151+
request, provider, error, exception, extra_context
152+
)

docker-app/qfieldcloud/core/admin.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from allauth.account.forms import EmailAwarePasswordResetTokenGenerator
1212
from allauth.account.models import EmailAddress
1313
from allauth.account.utils import user_pk_to_url_str
14-
from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
1514
from auditlog.admin import LogEntryAdmin as BaseLogEntryAdmin
1615
from auditlog.filters import ResourceTypeFilter
1716
from auditlog.models import ContentType, LogEntry
@@ -37,6 +36,8 @@
3736
from django.views.decorators.cache import never_cache
3837
from invitations.admin import InvitationAdmin as InvitationAdminBase
3938
from invitations.utils import get_invitation_model
39+
from rest_framework.authtoken.models import TokenProxy
40+
4041
from qfieldcloud.core import exceptions
4142
from qfieldcloud.core.models import (
4243
ApplyJob,
@@ -58,7 +59,6 @@
5859
from qfieldcloud.core.paginators import LargeTablePaginator
5960
from qfieldcloud.core.templatetags.filters import filesizeformat10
6061
from qfieldcloud.core.utils2 import delta_utils, jobs, pg_service_file
61-
from rest_framework.authtoken.models import TokenProxy
6262

6363
admin.site.unregister(LogEntry)
6464

@@ -147,9 +147,6 @@ def admin_urlname_by_obj(value, arg):
147147
# Unregister admins from other Django apps
148148
admin.site.unregister(Invitation)
149149
admin.site.unregister(TokenProxy)
150-
admin.site.unregister(SocialAccount)
151-
admin.site.unregister(SocialApp)
152-
admin.site.unregister(SocialToken)
153150
admin.site.unregister(EmailAddress)
154151

155152
UserEmailDetails = namedtuple(

docker-app/qfieldcloud/core/cron.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
from invitations.utils import get_invitation_model
88
from sentry_sdk import capture_message
99

10+
from qfieldcloud.filestorage.models import File
11+
1012
from ..core.models import ApplyJob, ApplyJobDelta, Delta, Job, Project
1113
from ..core.utils2 import storage
1214
from .invitations_utils import send_invitation
13-
from qfieldcloud.filestorage.models import File
14-
1515

1616
logger = logging.getLogger(__name__)
1717

docker-app/qfieldcloud/core/drf_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
from typing import Iterable
2+
13
from django.db.models import QuerySet
24
from rest_framework import filters, views
35
from rest_framework.request import Request
4-
from typing import Iterable
56

67

78
class QfcOrderingFilter(filters.OrderingFilter):

docker-app/qfieldcloud/core/fields.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from __future__ import annotations
22

3-
from typing import Protocol, cast, Any
43
from collections.abc import Callable
5-
from django.db import models
6-
from django.db.models.fields.files import FieldFile
4+
from typing import Any, Protocol, cast
5+
76
from django.core.files.storage import storages
8-
from django.db.models.fields.files import ImageFieldFile, ImageField
7+
from django.db import models
8+
from django.db.models.fields.files import FieldFile, ImageField, ImageFieldFile
99

1010

1111
class FileStorageNameModelProtocol(Protocol):

docker-app/qfieldcloud/core/invitations_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from invitations.adapters import get_invitations_adapter
1010
from invitations.signals import invite_url_sent
1111
from invitations.utils import get_invitation_model
12+
1213
from qfieldcloud.core import permissions_utils
1314
from qfieldcloud.core.models import Person
1415

0 commit comments

Comments
 (0)