Skip to content

Commit

Permalink
feat(socialaccount): SOCIALACCOUNT_ONLY
Browse files Browse the repository at this point in the history
  • Loading branch information
pennersr committed Apr 20, 2024
1 parent 5d5d287 commit af91d38
Show file tree
Hide file tree
Showing 15 changed files with 213 additions and 106 deletions.
3 changes: 3 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Note worthy changes
``/accounts/3rdparty/``. The old endpoints still work as redirects are in
place.

- Added a new setting, ``SOCIALACCOUNT_ONLY``, which when set to ``True``,
disables all functionality with respect to local accounts.


0.61.1 (2024-02-09)
*******************
Expand Down
2 changes: 2 additions & 0 deletions allauth/account/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class AccountConfig(AppConfig):
default_auto_field = app_settings.DEFAULT_AUTO_FIELD or "django.db.models.AutoField"

def ready(self):
from allauth.account import checks # noqa

required_mw = "allauth.account.middleware.AccountMiddleware"
if required_mw not in settings.MIDDLEWARE:
raise ImproperlyConfigured(
Expand Down
27 changes: 27 additions & 0 deletions allauth/account/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.core.checks import Critical, register


@register()
def settings_check(app_configs, **kwargs):
from allauth import app_settings as allauth_app_settings
from allauth.account import app_settings

ret = []
if allauth_app_settings.SOCIALACCOUNT_ONLY:
if app_settings.LOGIN_BY_CODE_ENABLED:
ret.append(
Critical(
msg="SOCIALACCOUNT_ONLY does not work with ACCOUNT_LOGIN_BY_CODE_ENABLED"
)
)
if allauth_app_settings.MFA_ENABLED:
ret.append(
Critical(msg="SOCIALACCOUNT_ONLY does not work with 'allauth.mfa'")
)
if app_settings.EMAIL_VERIFICATION != app_settings.EmailVerificationMethod.NONE:
ret.append(
Critical(
msg="SOCIALACCOUNT_ONLY requires ACCOUNT_EMAIL_VERIFICATION_METHOD = 'none'"
)
)
return ret
85 changes: 48 additions & 37 deletions allauth/account/urls.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,64 @@
from django.urls import path, re_path

from allauth import app_settings as allauth_app_settings
from allauth.account import app_settings

from . import views


urlpatterns = [
path("signup/", views.signup, name="account_signup"),
path("login/", views.login, name="account_login"),
path("logout/", views.logout, name="account_logout"),
path("reauthenticate/", views.reauthenticate, name="account_reauthenticate"),
path(
"password/change/",
views.password_change,
name="account_change_password",
),
path("password/set/", views.password_set, name="account_set_password"),
path("inactive/", views.account_inactive, name="account_inactive"),
# Email
path("email/", views.email, name="account_email"),
path(
"confirm-email/",
views.email_verification_sent,
name="account_email_verification_sent",
),
re_path(
r"^confirm-email/(?P<key>[-:\w]+)/$",
views.confirm_email,
name="account_confirm_email",
),
# password reset
path("password/reset/", views.password_reset, name="account_reset_password"),
path(
"password/reset/done/",
views.password_reset_done,
name="account_reset_password_done",
),
re_path(
r"^password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$",
views.password_reset_from_key,
name="account_reset_password_from_key",
),
path(
"password/reset/key/done/",
views.password_reset_from_key_done,
name="account_reset_password_from_key_done",
),
]

if not allauth_app_settings.SOCIALACCOUNT_ONLY:
urlpatterns.extend(
[
path("signup/", views.signup, name="account_signup"),
path(
"reauthenticate/", views.reauthenticate, name="account_reauthenticate"
),
# Email
path("email/", views.email, name="account_email"),
path(
"confirm-email/",
views.email_verification_sent,
name="account_email_verification_sent",
),
re_path(
r"^confirm-email/(?P<key>[-:\w]+)/$",
views.confirm_email,
name="account_confirm_email",
),
path(
"password/change/",
views.password_change,
name="account_change_password",
),
path("password/set/", views.password_set, name="account_set_password"),
# password reset
path(
"password/reset/", views.password_reset, name="account_reset_password"
),
path(
"password/reset/done/",
views.password_reset_done,
name="account_reset_password_done",
),
re_path(
r"^password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$",
views.password_reset_from_key,
name="account_reset_password_from_key",
),
path(
"password/reset/key/done/",
views.password_reset_from_key_done,
name="account_reset_password_from_key_done",
),
]
)

if app_settings.LOGIN_BY_CODE_ENABLED:
urlpatterns.extend(
[
Expand Down
8 changes: 7 additions & 1 deletion allauth/account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class LoginView(
@sensitive_post_parameters_m
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if allauth_app_settings.SOCIALACCOUNT_ONLY and request.method != "GET":
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)

def get_form_kwargs(self):
Expand All @@ -98,14 +100,17 @@ def form_valid(self, form):

def get_context_data(self, **kwargs):
ret = super().get_context_data(**kwargs)
signup_url = self.passthrough_next_url(reverse("account_signup"))
signup_url = None
if not allauth_app_settings.SOCIALACCOUNT_ONLY:
signup_url = self.passthrough_next_url(reverse("account_signup"))
site = get_current_site(self.request)

ret.update(
{
"signup_url": signup_url,
"site": site,
"SOCIALACCOUNT_ENABLED": allauth_app_settings.SOCIALACCOUNT_ENABLED,
"SOCIALACCOUNT_ONLY": allauth_app_settings.SOCIALACCOUNT_ONLY,
"LOGIN_BY_CODE_ENABLED": app_settings.LOGIN_BY_CODE_ENABLED,
}
)
Expand Down Expand Up @@ -171,6 +176,7 @@ def get_context_data(self, **kwargs):
"login_url": login_url,
"site": site,
"SOCIALACCOUNT_ENABLED": allauth_app_settings.SOCIALACCOUNT_ENABLED,
"SOCIALACCOUNT_ONLY": allauth_app_settings.SOCIALACCOUNT_ONLY,
}
)
return ret
Expand Down
6 changes: 6 additions & 0 deletions allauth/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ def SITES_ENABLED(self):
def SOCIALACCOUNT_ENABLED(self):
return apps.is_installed("allauth.socialaccount")

@property
def SOCIALACCOUNT_ONLY(self) -> bool:
from allauth.utils import get_setting

return get_setting("SOCIALACCOUNT_ONLY", False)

@property
def MFA_ENABLED(self):
return apps.is_installed("allauth.mfa")
Expand Down
26 changes: 9 additions & 17 deletions allauth/socialaccount/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,16 @@
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _

from allauth.account.adapter import get_adapter as get_account_adapter
from allauth.account.utils import user_email, user_field, user_username
from allauth.core.internal.adapter import BaseAdapter

from ..account.adapter import get_adapter as get_account_adapter
from ..account.app_settings import EmailVerificationMethod
from ..account.models import EmailAddress
from ..account.utils import user_email, user_field, user_username
from ..utils import (
from allauth.utils import (
deserialize_instance,
import_attribute,
serialize_instance,
valid_email_or_none,
)

from . import app_settings


Expand All @@ -42,6 +40,9 @@ class DefaultSocialAccountAdapter(BaseAdapter):
"invalid_token": _("Invalid token."),
"no_password": _("Your account has no password set up."),
"no_verified_email": _("Your account has no verified email address."),
"disconnect_last": _(
"You cannot disconnect your last remaining third-party account."
),
}

def pre_social_login(self, request, sociallogin):
Expand Down Expand Up @@ -142,21 +143,12 @@ def get_connect_redirect_url(self, request, socialaccount):
url = reverse("socialaccount_connections")
return url

def validate_disconnect(self, account, accounts):
def validate_disconnect(self, account, accounts) -> None:
"""
Validate whether or not the socialaccount account can be
safely disconnected.
"""
if len(accounts) == 1:
# No usable password would render the local account unusable
if not account.user.has_usable_password():
raise self.validation_error("no_password")
# No email address, no password reset
if app_settings.EMAIL_VERIFICATION == EmailVerificationMethod.MANDATORY:
if not EmailAddress.objects.filter(
user=account.user, verified=True
).exists():
raise self.validation_error("no_verified_email")
pass

def is_auto_signup_allowed(self, request, sociallogin):
# If email is specified, check for duplicate and if so, no auto signup.
Expand Down
2 changes: 1 addition & 1 deletion allauth/socialaccount/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def clean(self):
cleaned_data = super(DisconnectForm, self).clean()
account = cleaned_data.get("account")
if account:
get_adapter(self.request).validate_disconnect(account, self.accounts)
flows.connect.validate_disconnect(self.request, account)
return cleaned_data

def save(self):
Expand Down
28 changes: 28 additions & 0 deletions allauth/socialaccount/internal/flows/connect.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django.contrib import messages
from django.http import HttpResponseRedirect

from allauth import app_settings as allauth_settings
from allauth.account import app_settings as account_settings
from allauth.account.adapter import get_adapter as get_account_adapter
from allauth.account.models import EmailAddress
from allauth.account.reauthentication import (
raise_if_reauthentication_required,
reauthenticate_then_callback,
Expand All @@ -12,6 +14,32 @@
from allauth.socialaccount.models import SocialAccount, SocialLogin


def validate_disconnect(request, account):
"""
Validate whether or not the socialaccount account can be
safely disconnected.
"""
accounts = SocialAccount.objects.filter(user_id=account.user_id)
is_last = not accounts.exclude(pk=account.pk).exists()
if is_last:
adapter = get_adapter()
if allauth_settings.SOCIALACCOUNT_ONLY:
raise adapter.validation_error("disconnect_last")
# No usable password would render the local account unusable
if not account.user.has_usable_password():
raise adapter.validation_error("no_password")
# No email address, no password reset
if (
account_settings.EMAIL_VERIFICATION
== account_settings.EmailVerificationMethod.MANDATORY
):
if not EmailAddress.objects.filter(
user=account.user, verified=True
).exists():
raise adapter.validation_error("no_verified_email")
adapter.validate_disconnect(account, accounts)


def disconnect(request, account):
if account_settings.REAUTHENTICATION_REQUIRED:
raise_if_reauthentication_required(request)
Expand Down
43 changes: 25 additions & 18 deletions allauth/templates/account/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,31 @@
{% element h1 %}
{% trans "Sign In" %}
{% endelement %}
<p>
{% blocktrans %}If you have not created an account yet, then please
<a href="{{ signup_url }}">sign up</a> first.{% endblocktrans %}
</p>
{% url 'account_login' as login_url %}
{% element form form=form method="post" action=login_url tags="entrance,login" %}
{% slot body %}
{% csrf_token %}
{% element fields form=form unlabeled=True %}
{% endelement %}
{{ redirect_field }}
{% endslot %}
{% slot actions %}
{% element button type="submit" tags="prominent,login" %}
{% trans "Sign In" %}
{% endelement %}
{% endslot %}
{% endelement %}
{% if not SOCIALACCOUNT_ONLY %}
{% setvar link %}
<a href="{{ signup_url }}">
{% endsetvar %}
{% setvar end_link %}
</a>
{% endsetvar %}
<p>
{% blocktranslate %}If you have not created an account yet, then please {{ link }}sign up{{ end_link }} first.{% endblocktranslate %}
</p>
{% url 'account_login' as login_url %}
{% element form form=form method="post" action=login_url tags="entrance,login" %}
{% slot body %}
{% csrf_token %}
{% element fields form=form unlabeled=True %}
{% endelement %}
{{ redirect_field }}
{% endslot %}
{% slot actions %}
{% element button type="submit" tags="prominent,login" %}
{% trans "Sign In" %}
{% endelement %}
{% endslot %}
{% endelement %}
{% endif %}
{% if LOGIN_BY_CODE_ENABLED %}
{% element hr %}
{% endelement %}
Expand Down
Loading

0 comments on commit af91d38

Please sign in to comment.