diff --git a/.gitignore b/.gitignore index 538ef73e5af708403be0f1e6c5e620d5d2aace22..5a5849496455a33f92a446455710a28ed4d349e8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ MANIFEST .tox build .coverage +venv diff --git a/CHANGELOG b/CHANGELOG index dffa7b154619e6fbc7860485131ebf0aa4c9ab8c..fd4eadc3b01dfc82f71a302a133d70907b4611cf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ 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 ------------------ @@ -14,7 +19,8 @@ django-ucamlookup changelog 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 ------------------ diff --git a/README.md b/README.md index 442d0ad4f0e955b015a17edb44e28f1d726f3760..4928c8548e84e71dc5f4ae75df58ee4bfc3cf624 100644 --- a/README.md +++ b/README.md @@ -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 [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 Install django-ucamlookup using pip: @@ -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 diff --git a/README.rst b/README.rst deleted file mode 100644 index 14985f807481a25e942a5107a6297903d9124b16..0000000000000000000000000000000000000000 --- a/README.rst +++ /dev/null @@ -1,202 +0,0 @@ -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 diff --git a/setup.py b/setup.py index 7f700502cd5758674ad0799c0d14c74e6f858acf..2737e04ef86f87a3d3f09723982bcbefd2ea7429 100755 --- a/setup.py +++ b/setup.py @@ -6,9 +6,9 @@ from setuptools import find_packages setup( name='django-ucamlookup', 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', - version='1.9.5', + version='2.0.0', license='MIT', author='Information Systems Group, University Information Services, University of Cambridge', author_email='devops@uis.cam.ac.uk', diff --git a/ucamlookup/tests.py b/ucamlookup/tests.py index 5a3b8f012957d50e7ac3c7e6274db155dfd7ab3b..2a9ce80eec6374979f3967c3a9b01a48a69d62f7 100644 --- a/ucamlookup/tests.py +++ b/ucamlookup/tests.py @@ -6,15 +6,27 @@ try: from django.core.urlresolvers import reverse except Exception: from django.urls import reverse -from django.test import TestCase +from django.test import TestCase, override_settings from ucamlookup.models import LookupGroup 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, \ - validate_crsids + validate_crsids, get_connection 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): with self.assertRaises(User.DoesNotExist): User.objects.get(username="amc203") diff --git a/ucamlookup/utils.py b/ucamlookup/utils.py index c0a6552bfbf85b301611086e2bbf72044b401ef8..edcb37f687c3a1cb8aef82929059d155b9de46e3 100644 --- a/ucamlookup/utils.py +++ b/ucamlookup/utils.py @@ -1,22 +1,41 @@ import re + +from django.conf import settings 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): """ Returns the list of people based on the search string using the lookup ucam service :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)) 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 '' @@ -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 :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)) 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? if group is None: 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): :return: the list of group_ids """ 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)) except IbisException: return [] @@ -70,7 +89,7 @@ def get_user_lookupgroups(user): :return: the list of LookupGroups """ 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)) except IbisException: return [] @@ -82,14 +101,14 @@ def get_institutions(user=None): :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 all_institutions = list(filter(lambda institution: re.match(r'.*\d{2}$', institution.id) is None, all_institutions)) if user is not None: try: - all_institutions = PersonMethods(conn).getInsts("crsid", user.username) + all_institutions + all_institutions = PersonMethods(get_connection()).getInsts("crsid", user.username) + all_institutions except IbisException: pass @@ -100,7 +119,7 @@ def get_institution_name_by_id(institution_id, all_institutions=None): if all_institutions is not None: instname = next((institution[1] for institution in all_institutions if institution[0] == institution_id), None) 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 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): """ 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)))