This repository has been archived by the owner on Jun 19, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
IMPORTANT: this commit removes the previous LOOKUP_SELF setting and replaces it with LOOKUP_ROOT which points to the root of the lookupproxy API server. Deployments will need to be re-configured. Previously we (ab)used the token passed to us from the user to retrieve their lookup profile. The token is only conveniently present at authorisation time and so the authorisation logic was combined with fetching and caching the lookup resource for the current user. Aside from complication the authorisation flow, it slightly violated the spirit of OAuth2 tokens in that we were not accessing lookup *on behalf* of the user. We were, in fact, accessing lookup on behalf of the IAR backend application. We also encoded the lookup scheme and identifier in the username which, if not exactly brittle, was ugly. Re-work the lookup caching logic to be separate from the authentication logic: 1. Explicitly record the mapping between lookup scheme/identifier pairs and Django users by way of a new UserLookup model. 2. Separate the caching logic out into a new accessor function, assets.lookup.get_person_for_user(), which takes a Django user and returns the lookup person resource for that user. The resource is fetched from an in-memory cache if present or fetched from lookup if not. When fetching data from lookup, the IAR backend obtains its own token with a lookup:anonymous scope and uses that token to talk to lookup. The avoids the need to keep the user's token around. None of the existing permissions logic has been changed but now we explicitly ask for a lookup resource for a user *at the point we need it* rather than relying on it having been cached as part of the authentication flow. Since the user token no longer requires a lookup:anonymous scope, remove it from the Swagger UI. The default cached person resource lifetime is configurable through a setting. We update the scripts/create-client.sh script to create a backend client capable of requesting hydra.introspect and lookup:anonymous scopes and a client used for testing the UI.
- Loading branch information
Showing
19 changed files
with
330 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
""" | ||
Module providing lookup API-related functionality. | ||
""" | ||
from urllib.parse import urljoin | ||
import logging | ||
from django.conf import settings | ||
from django.core.cache import cache | ||
|
||
from .models import UserLookup | ||
from .oauth2client import AuthenticatedSession | ||
|
||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
#: An authenticated session which can access the lookup API | ||
LOOKUP_SESSION = AuthenticatedSession(scopes=settings.ASSETS_OAUTH2_LOOKUP_SCOPES) | ||
|
||
|
||
class LookupError(RuntimeError): | ||
""" | ||
Error raised if :py:func:`~.get_person_for_user` encounters a problem. | ||
""" | ||
pass | ||
|
||
|
||
def get_person_for_user(user): | ||
""" | ||
Return the resource from Lookup associated with the specified user. A requests package | ||
:py:class:`HTTPError` is raised if the request fails. | ||
The result of this function call is cached based on the username so it is safe to call this | ||
multiple times. | ||
If user is the anonymous user (user.is_anonymous is True), :py:class:`~.UserIsAnonymousError` | ||
is raised. | ||
""" | ||
# check that the user is not anonymous | ||
if user.is_anonymous: | ||
raise LookupError('User is anonymous') | ||
|
||
# check the user has an associated lookup identity | ||
if not UserLookup.objects.filter(user=user).exists(): | ||
raise LookupError('User has no lookup identity') | ||
|
||
# return a cached response if we have it | ||
cached_resource = cache.get("{user.username}:lookup".format(user=user)) | ||
if cached_resource is not None: | ||
return cached_resource | ||
|
||
# Extract the scheme and identifier for the token | ||
scheme = user.lookup.scheme | ||
identifier = user.lookup.identifier | ||
|
||
# Ask lookup about this person | ||
lookup_response = LOOKUP_SESSION.request( | ||
method='GET', url=urljoin( | ||
settings.LOOKUP_ROOT, | ||
'people/{scheme}/{identifier}?fetch=all_insts,all_groups'.format( | ||
scheme=scheme, identifier=identifier | ||
) | ||
) | ||
) | ||
|
||
# Raise if there was an error | ||
lookup_response.raise_for_status() | ||
|
||
# save cached value | ||
cache.set("{user.username}:lookup".format(user=user), lookup_response.json(), | ||
settings.LOOKUP_PEOPLE_CACHE_LIFETIME) | ||
|
||
# recurse, which should now retrieve the value from the cache | ||
return get_person_for_user(user) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Generated by Django 2.0.2 on 2018-03-06 12:31 | ||
|
||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('auth', '0009_alter_user_last_name_max_length'), | ||
('assets', '0008_auto_20180227_1035'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='UserLookup', | ||
fields=[ | ||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='lookup', serialize=False, to=settings.AUTH_USER_MODEL)), | ||
('scheme', models.CharField(max_length=255)), | ||
('identifier', models.CharField(max_length=255)), | ||
], | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from django.core.cache import cache | ||
|
||
|
||
def clear_cached_person_for_user(user): | ||
"""Explicitly clear the cached lookup response for a user.""" | ||
if not user.is_anonymous: | ||
cache.delete("{user.username}:lookup".format(user=user)) | ||
|
||
|
||
def set_cached_person_for_user(user, person): | ||
"""Explicitly set the cached lookup response for a user.""" | ||
if not user.is_anonymous: | ||
cache.set("{user.username}:lookup".format(user=user), person) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
I'm starting to wonder if it is now the time to create an APIUser that extends User and we put all the extra bits in there, like scheme, identifier, etc and use that as our default User model when using API authentication and use the normal User model when authenticating via Raven.