FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects
Commit f949ddb4 authored by Dr Rich Wareham's avatar Dr Rich Wareham
Browse files

Merge branch 'cudn-update' into 'master'

Support authenticated connections

Closes #5

See merge request !6
parents e2479e40 847717c5
No related branches found
No related tags found
1 merge request!6Support authenticated connections
Pipeline #1170 passed
...@@ -7,3 +7,4 @@ MANIFEST ...@@ -7,3 +7,4 @@ MANIFEST
.tox .tox
build build
.coverage .coverage
venv
django-ucamlookup changelog django-ucamlookup changelog
============================= =============================
2.0.0 - 12/03/2019
------------------
- Add support for authentication. This allows connectivity to the lookup API from outside the CUDN
1.9.5 - 19/12/2018 1.9.5 - 19/12/2018
------------------ ------------------
...@@ -14,7 +19,8 @@ django-ucamlookup changelog ...@@ -14,7 +19,8 @@ django-ucamlookup changelog
1.9.3 - 22/03/2017 1.9.3 - 22/03/2017
------------------ ------------------
- bugfix: "single select" control not initialising (introduced single_user on ucamlookup_users.html to initialise "single select") - bugfix: "single select" control not initialising (introduced single_user on ucamlookup_users.html to
initialise "single select")
1.9.2 - 18/01/2017 1.9.2 - 18/01/2017
------------------ ------------------
......
...@@ -4,6 +4,25 @@ django-ucamlookup is a library which provides useful methods and templates to in ...@@ -4,6 +4,25 @@ django-ucamlookup is a library which provides useful methods and templates to in
[Django](https://www.djangoproject.com/) application with the University of Cambridge University [Django](https://www.djangoproject.com/) application with the University of Cambridge University
[Lookup service](https://www.lookup.cam.ac.uk/). [Lookup service](https://www.lookup.cam.ac.uk/).
## Configuration
The following parameters are optional configurations that you can use in your django settings.
* ``UCAMLOOKUP_HOST``. Optional. Default: ``"www.lookup.cam.ac.uk"``. Specifies the hostname used for the
IbisClientConnection. This is the connection object that will be used to make all API calls related to lookup.
* ``UCAMLOOKUP_PORT``. Optional. Default: ``443``. Specifies the port used for the
IbisClientConnection. This is the connection object that will be used to make all API calls related to lookup.
* ``UCAMLOOKUP_URL_BASE``. Optional. Default: ``""``. Specifies the URL base used for the
IbisClientConnection. This is the connection object that will be used to make all API calls related to lookup.
* ``UCAMLOOKUP_CHECK_CERTS``. Optional. Default: ``True``. Indicates if the client should check if the server side
certificates are valid.
* ``UCAMLOOKUP_USERNAME``. Optional. Default: ``None``. Specifies the username used for the
IbisClientConnection. This is the connection object that will be used to make all API calls related to lookup. We
recommend the use of Lookup groups for authentication instead of an individual Raven account.
* ``UCAMLOOKUP_PASSWORD``. Optional. Default: ``None``. Specifies the password used for the
IbisClientConnection. This is the connection object that will be used to make all API calls related to lookup. We
recommend the use of Lookup groups passwords for authentication instead of an individual Raven account password.
## Use ## Use
Install django-ucamlookup using pip: Install django-ucamlookup using pip:
...@@ -33,11 +52,21 @@ and the urls entries in the urls.py file: ...@@ -33,11 +52,21 @@ and the urls entries in the urls.py file:
) )
``` ```
## Requirements and warning ## Warning
Lookup contains personal data of University of Cambridge members. Make sure that you are only showing this data to
users with rights to see this data.
## Networking
If no optional settings are specified, django-ucamlookup will use ``anonymous`` as username and no password when
setting up an IbisClientConnection and executing Lookup APIs. This type of anonymous conneciton is only available
inside the Cambridge University Network (CUDN). If your application is deployed outside the CUDN you should use the
optional authentication with ``UCAMLOOKUP_USERNAME`` and ``UCAMLOOKUP_PASSWORD``.
We do not recommend the use of individual Raven accounts and instead to set up a Lookup group. Users can generate a
password for the group and use the group short name as a username for authentication.
This module will only work inside the University of Cambridge network. Make sure your users are authenticated as
University of Cambridge users (you can use the django-ucamwebauth module) or you will be exposing personal data to
non authorised users.
## Lookup User ## Lookup User
......
Introduction
============
django-ucamlookup is a library which provides useful methods and
templates to integrate your `Django <https://www.djangoproject.com/>`__
application with the University of Cambridge University `Lookup
service <https://www.lookup.cam.ac.uk/>`__.
Use
---
Install django-ucamlookup using pip:
.. code:: bash
pip install django-ucamlookup
Add django-ucamlookup to your installed applications in your project
configuration settings.py:
.. code:: python
INSTALLED_APPS=(
...
'ucamlookup',
...
),
and the urls entries in the urls.py file:
.. code:: python
urlpatterns = patterns(
...
# lookup/ibis urls
url(r'^ucamlookup/', include('ucamlookup.urls')),
...
)
Requirements and warning
------------------------
This module will only work inside the University of Cambridge network.
Make sure your users are authenticated as University of Cambridge users
(you can use the django-ucamwebauth module) or you will be exposing
personal data to non authorised users.
Lookup User
-----------
django-ucamlookup modifies a User object each time is going to be saved,
either new or update, and assigns to its *last\_name* property the
visible name from lookup for that user. The username is used to search
for this user in lookup.
Lookup Group
------------
django-ucamlookup includes a new model called LookupGroup that it is
used to cache lookup models. It is used to store the lookup group id and
its name, and therefore used to reduce the number of call to the lookup
service. It can also be used to create relation with other models. For
example, let's say we have a model called Secret and we only want to let
access to it to users inside a certain group or groups. We will create a
ManyToMany relation from Secrets to LookupGroup.
The name of the group is retrieve from the lookup service each time the
group is saved (new or updated). The name is stored in the *name*
property of the class and the id of the lookup group is stored in
*lookup\_id*.
It is important to say that this model is not used to cache relations
between lookup users and lookup groups. These relations are always
queried to the live lookup service. The model is only used to let the
developer make relations between models that include lookup groups and
cache the name of the group.
Template macros
---------------
Two macros are available to be used in a template: ucamlookup\_users,
and ucamlookup\_groups. These macros have javascript functions that will
modify a html input tag to an interactive ajax box with interaction to
the lookup service that will let the user use autocomplete and search
for lookup users and groups.
If you want to include an input box to let the user search and introduce
a single user or a list of users, use the ucamlookup\_users macro. You
should pass as parameters to the macro the html input tag *id* that you
want to modify/use and if you want to let the user select one or more
users with the parameter *multiple*:
.. code:: python
{% include 'ucamlookup_users.html' with input_tag_id="lookup_users" multiple=true user_list="authors" %}
If you want to show existing records in the input tag you will need to
pass to the view the list of crsids. This list needs to be passed inside
a dictionary called *loockup\_lists*. The key entry name of the
dictionary where the list is located it is passed to the macro using the
variable *user\_list* as shown previously. In this example:
.. code:: python
lookup_lists = {
'authors': post.users.all(),
}
You will also have to include the following macro in the head of your
template to load the js and css files associated. These macros require
jquery if you want to include your own jquery library or you are already
using it in your template use the parameter *jquery* to specify it.
.. code:: python
{% include 'ucamlookup_headers.html' with jquery=True %}
And your input tag will be transform into an ajax box that allows the
user to search for users using lookup either using their username or
their complete name. A list of crsids will be sent as the value of the
input tag.
The same will work for lookup groups, just substitute user by group in
the id and in the include.
Admin interface
---------------
The admin interface is tunned to add managing options for the
LookupGroup model. The *add* option will show the same
ajax-lookup-integrated-input as the template macros described above.
It also changes the add form for the user and it also shows an
interactive ajax lookup-integrated input form when the admin wants to
add a new user to the app.
These input forms allow to search for name and crsid in the case of a
new user and for name in the case of a lookup group.
Available functions
-------------------
The module also provides some useful functions to use in your app that
do all the calls to the lookup service needed.
*get\_group\_ids\_of\_a\_user\_in\_lookup(user)*: Returns the list of
group ids of a user
*user\_in\_groups(user, lookup\_groups)*: Check in the lookup webservice
if the user is member of any of the groups in the LookupGroup list
passed by parameter. Returns True if the user is in any of the groups or
False otherwise
*def get\_institutions(user=None)*: Returns the list of institutions
using the lookup ucam service. The institutions of the user passed by
parameters will be shown first in the list returned
*validate\_crsids(crsids\_text)*: It receives a list of crsids (comming
from input tag from the template macros described previously) [wich
format is separated by commas and with no spaces in between] and returns
a list of Users corresponding to the crsids passed.
*get\_or\_create\_user\_by\_crsid(crsid)*: Returns the User
corresponding to the crsid passed. If it does not exists in the
database, it is created.
*get\_institution\_name\_by\_id(institution\_id,
all\_institutions=None)*: Returns the name of an institution by the id
passed. If all\_institutions is passed (the result from
get\_institutions) then the search is done locally using this list
instead of a lookup call.
The last two methods can be used to add institutions to a model and show
the name instead of the code in the admin interface
.. code:: python
class MyModelAdmin(ModelAdmin):
all_institutions = get_institutions()
model = MyModel
list_display = ('institution', )
list_filter = ('institution_id', )
def institution(self, obj):
return get_institution_name_by_id(obj.institution_id, self.all_institutions)
institution.admin_order_field = 'institution_id'
Developing
==========
Run tests
---------
Tox is configured to run on a container with a matrix execution of different versions of python and django combined.
It will also show the coverage and any possible PEP8 violations.
.. code:: shell
$ docker-compose up
...@@ -6,9 +6,9 @@ from setuptools import find_packages ...@@ -6,9 +6,9 @@ from setuptools import find_packages
setup( setup(
name='django-ucamlookup', name='django-ucamlookup',
description='A Django module for the University of Cambridge Lookup service', description='A Django module for the University of Cambridge Lookup service',
long_description=open('README.rst').read(), long_description=open('README.md').read(),
url='https://github.com/uisautomation/django-ucamlookup.git', url='https://github.com/uisautomation/django-ucamlookup.git',
version='1.9.5', version='2.0.0',
license='MIT', license='MIT',
author='Information Systems Group, University Information Services, University of Cambridge', author='Information Systems Group, University Information Services, University of Cambridge',
author_email='devops@uis.cam.ac.uk', author_email='devops@uis.cam.ac.uk',
......
...@@ -6,15 +6,27 @@ try: ...@@ -6,15 +6,27 @@ try:
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
except Exception: except Exception:
from django.urls import reverse from django.urls import reverse
from django.test import TestCase from django.test import TestCase, override_settings
from ucamlookup.models import LookupGroup from ucamlookup.models import LookupGroup
from ucamlookup.utils import user_in_groups, get_users_from_query, return_visibleName_by_crsid, get_groups_from_query, \ from ucamlookup.utils import user_in_groups, get_users_from_query, return_visibleName_by_crsid, get_groups_from_query, \
return_title_by_groupid, get_group_ids_of_a_user_in_lookup, get_institutions, get_institution_name_by_id, \ return_title_by_groupid, get_group_ids_of_a_user_in_lookup, get_institutions, get_institution_name_by_id, \
validate_crsids validate_crsids, get_connection
class UcamLookupTests(TestCase): class UcamLookupTests(TestCase):
@override_settings(UCAMLOOKUP_HOST="mock_host", UCAMLOOKUP_PORT=80, UCAMLOOKUP_URL_BASE="/mock",
UCAMLOOKUP_CHECK_CERTS=False, UCAMLOOKUP_USERNAME="mock_username",
UCAMLOOKUP_PASSWORD="mock_password")
def test_optional_settins(self):
conn = get_connection()
self.assertEqual(conn.host, "mock_host")
self.assertEqual(conn.port, 80)
self.assertEqual(conn.url_base, "/mock/")
self.assertIsNone(conn.ca_certs)
self.assertEqual(conn.username, "mock_username")
self.assertEqual(conn.password, "mock_password")
def test_add_name_to_user_and_add_title_to_group(self): def test_add_name_to_user_and_add_title_to_group(self):
with self.assertRaises(User.DoesNotExist): with self.assertRaises(User.DoesNotExist):
User.objects.get(username="amc203") User.objects.get(username="amc203")
......
import re import re
from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from ucamlookup.ibisclient import createConnection, PersonMethods, GroupMethods, IbisException, InstitutionMethods from ucamlookup.ibisclient import PersonMethods, GroupMethods, IbisException, InstitutionMethods, \
IbisClientConnection
def get_connection():
UCAMLOOKUP_HOST = getattr(settings, "UCAMLOOKUP_HOST", "www.lookup.cam.ac.uk")
UCAMLOOKUP_PORT = getattr(settings, 'UCAMLOOKUP_PORT', 443)
UCAMLOOKUP_URL_BASE = getattr(settings, 'UCAMLOOKUP_URL_BASE', "")
UCAMLOOKUP_CHECK_CERTS = getattr(settings, 'UCAMLOOKUP_CHECK_CERTS', True)
UCAMLOOKUP_USERNAME = getattr(settings, 'UCAMLOOKUP_USERNAME', None)
UCAMLOOKUP_PASSWORD = getattr(settings, 'UCAMLOOKUP_PASSWORD', None)
conn = IbisClientConnection(UCAMLOOKUP_HOST, UCAMLOOKUP_PORT, UCAMLOOKUP_URL_BASE, UCAMLOOKUP_CHECK_CERTS)
if UCAMLOOKUP_USERNAME:
conn.set_username(UCAMLOOKUP_USERNAME)
if UCAMLOOKUP_PASSWORD:
conn.set_password(UCAMLOOKUP_PASSWORD)
conn = createConnection() return conn
def get_users_from_query(search_string): def get_users_from_query(search_string):
""" Returns the list of people based on the search string using the lookup ucam service """ Returns the list of people based on the search string using the lookup ucam service
:param search_string: the search string :param search_string: the search string
""" """
persons = PersonMethods(conn).search(query=search_string) persons = PersonMethods(get_connection()).search(query=search_string)
return list(map((lambda person: {'crsid': person.identifier.value, 'visibleName': person.visibleName}), persons)) return list(map((lambda person: {'crsid': person.identifier.value, 'visibleName': person.visibleName}), persons))
def return_visibleName_by_crsid(crsid): def return_visibleName_by_crsid(crsid):
person = PersonMethods(conn).getPerson(scheme='crsid', identifier=crsid) person = PersonMethods(get_connection()).getPerson(scheme='crsid', identifier=crsid)
return person.visibleName if person is not None else '' return person.visibleName if person is not None else ''
...@@ -24,13 +43,13 @@ def get_groups_from_query(search_string): ...@@ -24,13 +43,13 @@ def get_groups_from_query(search_string):
""" Returns the list of groups based on the search string using the lookup ucam service """ Returns the list of groups based on the search string using the lookup ucam service
:param search_string: the search string :param search_string: the search string
""" """
groups = GroupMethods(conn).search(query=search_string) groups = GroupMethods(get_connection()).search(query=search_string)
return list(map((lambda group: {'groupid': group.groupid, 'title': group.title}), groups)) return list(map((lambda group: {'groupid': group.groupid, 'title': group.title}), groups))
def return_title_by_groupid(groupid): def return_title_by_groupid(groupid):
group = GroupMethods(conn).getGroup(groupid=groupid) group = GroupMethods(get_connection()).getGroup(groupid=groupid)
# TODO If a group does not exists in lookup should we allow it? # TODO If a group does not exists in lookup should we allow it?
if group is None: if group is None:
raise ValidationError("The group with id %(groupid)s does not exist in Lookup", code='invalid', raise ValidationError("The group with id %(groupid)s does not exist in Lookup", code='invalid',
...@@ -44,7 +63,7 @@ def get_group_ids_of_a_user_in_lookup(user): ...@@ -44,7 +63,7 @@ def get_group_ids_of_a_user_in_lookup(user):
:return: the list of group_ids :return: the list of group_ids
""" """
try: try:
group_list = PersonMethods(conn).getGroups(scheme="crsid", identifier=user.username) group_list = PersonMethods(get_connection()).getGroups(scheme="crsid", identifier=user.username)
return list(map(lambda group: group.groupid, group_list)) return list(map(lambda group: group.groupid, group_list))
except IbisException: except IbisException:
return [] return []
...@@ -70,7 +89,7 @@ def get_user_lookupgroups(user): ...@@ -70,7 +89,7 @@ def get_user_lookupgroups(user):
:return: the list of LookupGroups :return: the list of LookupGroups
""" """
try: try:
group_list = PersonMethods(conn).getGroups(scheme="crsid", identifier=user.username) group_list = PersonMethods(get_connection()).getGroups(scheme="crsid", identifier=user.username)
return list(map(lambda group: get_or_create_group_by_groupid(group.groupid), group_list)) return list(map(lambda group: get_or_create_group_by_groupid(group.groupid), group_list))
except IbisException: except IbisException:
return [] return []
...@@ -82,14 +101,14 @@ def get_institutions(user=None): ...@@ -82,14 +101,14 @@ def get_institutions(user=None):
:param user: the user doing the request :param user: the user doing the request
""" """
all_institutions = InstitutionMethods(conn).allInsts(includeCancelled=False) all_institutions = InstitutionMethods(get_connection()).allInsts(includeCancelled=False)
# filter all the institutions that were created for store year students # filter all the institutions that were created for store year students
all_institutions = list(filter(lambda institution: re.match(r'.*\d{2}$', institution.id) is None, all_institutions = list(filter(lambda institution: re.match(r'.*\d{2}$', institution.id) is None,
all_institutions)) all_institutions))
if user is not None: if user is not None:
try: try:
all_institutions = PersonMethods(conn).getInsts("crsid", user.username) + all_institutions all_institutions = PersonMethods(get_connection()).getInsts("crsid", user.username) + all_institutions
except IbisException: except IbisException:
pass pass
...@@ -100,7 +119,7 @@ def get_institution_name_by_id(institution_id, all_institutions=None): ...@@ -100,7 +119,7 @@ def get_institution_name_by_id(institution_id, all_institutions=None):
if all_institutions is not None: if all_institutions is not None:
instname = next((institution[1] for institution in all_institutions if institution[0] == institution_id), None) instname = next((institution[1] for institution in all_institutions if institution[0] == institution_id), None)
else: else:
institution = InstitutionMethods(conn).getInst(instid=institution_id) institution = InstitutionMethods(get_connection()).getInst(instid=institution_id)
instname = institution.name if institution is not None else None instname = institution.name if institution is not None else None
return instname if instname is not None else 'This institution no longer exists in the database' return instname if instname is not None else 'This institution no longer exists in the database'
...@@ -168,4 +187,4 @@ def get_users_of_a_group(group): ...@@ -168,4 +187,4 @@ def get_users_of_a_group(group):
""" """
return list(map(lambda user: get_or_create_user_by_crsid(user.identifier), return list(map(lambda user: get_or_create_user_by_crsid(user.identifier),
GroupMethods(conn).getMembers(groupid=group.groupid))) GroupMethods(get_connection()).getMembers(groupid=group.groupid)))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment