Skip to content

Commit

Permalink
Add auto permission backend (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
caspervdw authored Nov 14, 2023
1 parent 81e19af commit 19ab163
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 4 deletions.
21 changes: 20 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ First-time logins

For first-time logins, there is no RemoteUser object to match the external
user ID with a local django user. In this case, users are accepted only if the
user presents a valid invitation. This is because there is no way to safely
user presents a valid invitation (or when using ``TrustedProviderMigrationBackend``, see below).
This is because there is no way to safely
match external user ids to local django users.

There are two kinds of invitations: invitations with user, and invitations
Expand Down Expand Up @@ -227,6 +228,16 @@ users are matched by username. On first-time login, a RemoteUser object is
created to link the external and local users permanently.


Auto-accepting users
--------------------

For some sites we might want to automatically create local users if they log in
from a trusted identity provider. For such sites, enable the
``TrustedProviderMigrationBackend`` and add a ``NENS_AUTH_TRUSTED_PROVIDERS_NEW_USERS`` setting.
The setting contains the list of provider names (as configured in cognito) that we trust to
have correct email addresses.


Auto-accepting N&S users
------------------------

Expand Down Expand Up @@ -261,6 +272,14 @@ There is no check on ``email_verified`` as that turns out to be hard to
configure.


Auto-assigning permissions
--------------------------

Any user that logs in can automatically be assigned permissions. This can be
implemented in the ``auto_assign(user, claims)`` method of a custom permission class,
which needs to be set on the ``NENS_AUTH_PERMISSION_BACKEND`` setting.


Bearer tokens (optional)
------------------------

Expand Down
6 changes: 5 additions & 1 deletion nens_auth_client/backends.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .users import _extract_provider_name
from .users import create_remote_user
from .users import create_user
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
Expand Down Expand Up @@ -162,7 +163,10 @@ def authenticate(self, request, claims):
try:
user = UserModel.objects.get(email__iexact=email)
except ObjectDoesNotExist:
return
if provider_name in settings.NENS_AUTH_TRUSTED_PROVIDERS_NEW_USERS:
return create_user(claims)
else:
return
except MultipleObjectsReturned:
raise PermissionDenied(settings.NENS_AUTH_ERROR_USER_MULTIPLE)

Expand Down
12 changes: 12 additions & 0 deletions nens_auth_client/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,15 @@ def check_error_message_formatting(app_configs=None, **kwargs):
)

return errors


@register()
def check_trusted_providers(app_configs=None, **kwargs):
trusted = set(settings.TRUSTED_PROVIDERS)
trusted_new = set(settings.TRUSTED_PROVIDERS_NEW_USERS)

if not trusted.issuperset(trusted_new):
return [
Error("TRUSTED_PROVIDERS must be a superset of TRUSTED_PROVIDERS_NEW_USERS")
]
return []
1 change: 1 addition & 0 deletions nens_auth_client/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class NensAuthClientAppConf(AppConf):
USERNAME_MAX_LENGTH = 50

TRUSTED_PROVIDERS = [] # providerName's trusted by TrustedProviderMigrationBackend
TRUSTED_PROVIDERS_NEW_USERS = [] # providerName's trusted for creating new users

ERROR_USER_DOES_NOT_EXIST = "No user account available for these credentials."
ERROR_USER_INACTIVE = "This account was set to inactive."
Expand Down
4 changes: 2 additions & 2 deletions nens_auth_client/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# (c) Nelen & Schuurmans. Proprietary, see LICENSE file.
from . import permissions
from .signals import invitation_accepted
from datetime import timedelta
from django.conf import settings
Expand Down Expand Up @@ -168,7 +169,6 @@ def check_acceptability(self, email=None):
return True

def accept(self, user, **kwargs):
backend = import_string(settings.NENS_AUTH_PERMISSION_BACKEND)()
if self.user_id and self.user_id != user.id:
raise PermissionDenied(
settings.NENS_AUTH_ERROR_INVITATION_WRONG_USER.format(
Expand All @@ -177,7 +177,7 @@ def accept(self, user, **kwargs):
)
)
try:
result = backend.assign(
result = permissions.assign_permissions(
permissions=json.loads(self.permissions), user=user, **kwargs
)
except Exception:
Expand Down
21 changes: 21 additions & 0 deletions nens_auth_client/permissions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# (c) Nelen & Schuurmans. Proprietary, see LICENSE file.
from django.conf import settings
from django.contrib.auth.models import Permission
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.utils.module_loading import import_string

import logging

Expand Down Expand Up @@ -72,3 +74,22 @@ def assign(self, permissions, user):
"Skipped assigning non-existing permission %s", permission_key
)
user.user_permissions.add(*user_permission_objs)

def auto_assign(self, user, claims):
# assign permissions based on the user's claims
# does nothing by default
return None


def get_permission_backend():
return import_string(settings.NENS_AUTH_PERMISSION_BACKEND)()


def assign_permissions(permissions, user, **kwargs):
return get_permission_backend().assign(permissions=permissions, user=user, **kwargs)


def auto_assign_permissions(user, claims):
backend = get_permission_backend()
if hasattr(backend, "auto_assign"):
backend.auto_assign(user=user, claims=claims)
9 changes: 9 additions & 0 deletions nens_auth_client/tests/test_authorize_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,19 @@ def invitation_getter(mocker):
return manager.select_related.return_value.get


@pytest.fixture
def permissions_m(mocker):
return mocker.patch("nens_auth_client.views.permissions")


def test_authorize(
id_token_generator,
auth_req_generator,
rq_mocker,
openid_configuration,
users_m,
login_m,
permissions_m,
):
id_token, claims = id_token_generator(testclaim="bar")
user = User(username="testuser")
Expand Down Expand Up @@ -72,6 +78,9 @@ def test_authorize(
assert args[0] == claims
assert args[1].keys() == {"id_token"}

# check if auto_assign_permissions was called
permissions_m.auto_assign_permissions.assert_called_once_with(user, claims)


def test_authorize_no_user(id_token_generator, auth_req_generator, users_m, login_m):
id_token, claims = id_token_generator()
Expand Down
23 changes: 23 additions & 0 deletions nens_auth_client/tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ def create_remote_user(mocker):
return mocker.patch("nens_auth_client.backends.create_remote_user")


@pytest.fixture
def create_user(mocker):
return mocker.patch("nens_auth_client.backends.create_user")


def test_remote_user_exists(user_getter):
user_getter.return_value = User(username="testuser")

Expand Down Expand Up @@ -398,3 +403,21 @@ def test_trusted_backend_proper_prerequisites(claims):
request=None, claims=claims
)
assert user is None


def test_trusted_backend_create_new_user(user_getter, create_user, settings):
# User does not exist and TRUSTED_PROVIDERS_NEW_USERS set? Create new user.
settings.NENS_AUTH_TRUSTED_PROVIDERS = ["vanrees"]
settings.NENS_AUTH_TRUSTED_PROVIDERS_NEW_USERS = ["vanrees"]
claims = {
"sub": "remote-uid",
"cognito:username": "goede.klant",
"email": "[email protected]",
"identities": [{"providerName": "vanrees"}],
}
user_getter.side_effect = ObjectDoesNotExist
user = backends.TrustedProviderMigrationBackend().authenticate(
request=None, claims=claims
)
assert user == create_user.return_value
create_user.assert_called_once_with(claims)
4 changes: 4 additions & 0 deletions nens_auth_client/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# (c) Nelen & Schuurmans. Proprietary, see LICENSE file.
# from nens_auth_client import models
from . import permissions
from . import users
from .backends import RemoteUserBackend
from .models import Invitation
Expand Down Expand Up @@ -207,6 +208,9 @@ def authorize(request):
users.update_user(user, claims)
users.update_remote_user(claims, tokens)

# Automatically assign permissions based on the user's claims
permissions.auto_assign_permissions(user, claims)

# Log the user in
django_auth.login(request, user)

Expand Down

0 comments on commit 19ab163

Please sign in to comment.