diff --git a/cas_server/tests/test_view.py b/cas_server/tests/test_view.py index 32b79c9..f1bdb85 100644 --- a/cas_server/tests/test_view.py +++ b/cas_server/tests/test_view.py @@ -31,21 +31,25 @@ class LoginTestCase(TestCase, BaseServicePattern): """Tests for the login view""" def setUp(self): """Prepare the test context:""" + # we prepare a bunch a service url and service patterns for tests self.setup_service_patterns() def assert_logged(self, client, response, warn=False, code=200): """Assertions testing that client is well authenticated""" self.assertEqual(response.status_code, code) + # this message is displayed to the user upon successful authentication self.assertTrue( ( b"You have successfully logged into " b"the Central Authentication Service" ) in response.content ) + # these session variables a set if usccessfully authenticated self.assertTrue(client.session["username"] == settings.CAS_TEST_USER) self.assertTrue(client.session["warn"] is warn) self.assertTrue(client.session["authenticated"] is True) + # on successfull authentication, a corresponding user object is created self.assertTrue( models.User.objects.get( username=settings.CAS_TEST_USER, @@ -56,6 +60,8 @@ class LoginTestCase(TestCase, BaseServicePattern): def assert_login_failed(self, client, response, code=200): """Assertions testing a failed login attempt""" self.assertEqual(response.status_code, code) + # this message is displayed to the user upon successful authentication, so it should not + # appear self.assertFalse( ( b"You have successfully logged into " @@ -63,67 +69,101 @@ class LoginTestCase(TestCase, BaseServicePattern): ) in response.content ) + # if authentication has failed, these session variables should not be set self.assertTrue(client.session.get("username") is None) self.assertTrue(client.session.get("warn") is None) self.assertTrue(client.session.get("authenticated") is None) def test_login_view_post_goodpass_goodlt(self): """Test a successul login""" + # we get a client who fetch a frist time the login page and the login form default + # parameters client, params = get_login_page_params() + # we set username/password in the form params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD + # the LoginTicket in the form should match a valid LT in the user session self.assertTrue(params['lt'] in client.session['lt']) + # we post a login attempt response = client.post('/login', params) + # as username/password/lt are all valid, the login should succed self.assert_logged(client, response) - # LoginTicket conssumed + # The LoginTicket is conssumed and should no longer be valid self.assertTrue(params['lt'] not in client.session['lt']) def test_login_view_post_goodpass_goodlt_warn(self): """Test a successul login requesting to be warned before creating services tickets""" + # get a client and initial login params client, params = get_login_page_params() + # set valids usernames/passswords params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD + # this time, we check the warn checkbox params["warn"] = "on" + # postings login request response = client.post('/login', params) + # as username/password/lt are all valid, the login should succed and warn be enabled self.assert_logged(client, response, warn=True) def test_lt_max(self): """Check we only keep the last 100 Login Ticket for a user""" + # get a client and initial login params client, params = get_login_page_params() + # get a first LT that should be valid current_lt = params["lt"] + # we keep the last 100 generated LT by user, so after having generated `i_in_test` we + # test if `current_lt` is still valid i_in_test = random.randint(0, 99) + # after `i_not_in_test` `current_lt` should be valid not more i_not_in_test = random.randint(101, 150) + # start generating 150 LT for i in range(150): if i == i_in_test: + # before more than 100 LT generated, the first TL should be valid self.assertTrue(current_lt in client.session['lt']) if i == i_not_in_test: + # after more than 100 LT generated, the first LT should be valid no more self.assertTrue(current_lt not in client.session['lt']) + # assert that we do not keep more that 100 valid LT self.assertTrue(len(client.session['lt']) <= 100) + # generate a new LT by getting the login page client, params = get_login_page_params(client) + # in the end, we still have less that 100 valid LT self.assertTrue(len(client.session['lt']) <= 100) def test_login_view_post_badlt(self): - """Login attempt with a bad LoginTicket""" + """Login attempt with a bad LoginTicket, login should fail""" + # get a client and initial login params client, params = get_login_page_params() + # set valid username/password params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD + # set a bad LT params["lt"] = 'LT-random' + # posting the login request response = client.post('/login', params) + # as the LT is not valid, login should fail self.assert_login_failed(client, response) + # the reason why login has failed is displayed to the user self.assertTrue(b"Invalid login ticket" in response.content) def test_login_view_post_badpass_good_lt(self): """Login attempt with a bad password""" + # get a client and initial login params client, params = get_login_page_params() + # set valid username but invalid password params["username"] = settings.CAS_TEST_USER params["password"] = "test2" + # posting the login request response = client.post('/login', params) + # as the password is wrong, login should fail self.assert_login_failed(client, response) + # the reason why login has failed is displayed to the user self.assertTrue( ( b"The credentials you provided cannot be " @@ -133,19 +173,28 @@ class LoginTestCase(TestCase, BaseServicePattern): def assert_ticket_attributes(self, client, ticket_value): """check the ticket attributes in the db""" + # Get get current session user in the db user = models.User.objects.get( username=settings.CAS_TEST_USER, session_key=client.session.session_key ) + # we should find exactly one user self.assertTrue(user) + # get the ticker object corresponting to `ticket_value` ticket = models.ServiceTicket.objects.get(value=ticket_value) + # chek that the ticket is well attributed to the user self.assertEqual(ticket.user, user) + # check that the user attributes match the attributes registered on the ticket self.assertEqual(ticket.attributs, settings.CAS_TEST_ATTRIBUTES) + # check that the ticket has not being validated yet self.assertEqual(ticket.validate, False) + # check that the service pattern registered on the ticket is the on we use for tests self.assertEqual(ticket.service_pattern, self.service_pattern) def assert_service_ticket(self, client, response): """check that a ticket is well emited when requested on a allowed service""" + # On ticket emission, we should be redirected to the service url, setting the ticket + # GET parameter self.assertEqual(response.status_code, 302) self.assertTrue(response.has_header('Location')) self.assertTrue( @@ -153,15 +202,21 @@ class LoginTestCase(TestCase, BaseServicePattern): "https://www.example.com?ticket=%s-" % settings.CAS_SERVICE_TICKET_PREFIX ) ) - + # check that the value of the ticket GET parameter match the value of the ticket + # created in the db ticket_value = response['Location'].split('ticket=')[-1] self.assert_ticket_attributes(client, ticket_value) def test_view_login_get_allowed_service(self): """Request a ticket for an allowed service by an unauthenticated client""" + # get a bare new http client client = Client() + # we are not authenticated and are asking for a ticket for https://www.example.com + # which is a valid service matched by self.service_pattern response = client.get("/login?service=https://www.example.com") + # the login page should be displayed self.assertEqual(response.status_code, 200) + # we warn the user why it need to authenticated self.assertTrue( ( b"Authentication required by service " @@ -171,23 +226,34 @@ class LoginTestCase(TestCase, BaseServicePattern): def test_view_login_get_denied_service(self): """Request a ticket for an denied service by an unauthenticated client""" + # get a bare new http client client = Client() + # we are not authenticated and are asking for a ticket for https://www.example.net + # which is NOT a valid service response = client.get("/login?service=https://www.example.net") self.assertEqual(response.status_code, 200) + # we warn the user that https://www.example.net is not an allowed service url self.assertTrue(b"Service https://www.example.net non allowed" in response.content) def test_view_login_get_auth_allowed_service(self): """Request a ticket for an allowed service by an authenticated client""" - # client is already authenticated + # get a client that is already authenticated client = get_auth_client() + # ask for a ticket for https://www.example.com response = client.get("/login?service=https://www.example.com") + # as https://www.example.com is a valid service a ticket should be created and the + # user redirected to the service url self.assert_service_ticket(client, response) def test_view_login_get_auth_allowed_service_warn(self): """Request a ticket for an allowed service by an authenticated client""" - # client is already authenticated + # get a client that is already authenticated and has ask to be warned befor we + # generated a ticket client = get_auth_client(warn="on") + # ask for a ticket for https://www.example.com response = client.get("/login?service=https://www.example.com") + # we display a warning to the user, asking him to validate the ticket creation (insted + # a generating and redirecting directly to the service url) self.assertEqual(response.status_code, 200) self.assertTrue( ( @@ -195,42 +261,63 @@ class LoginTestCase(TestCase, BaseServicePattern): b"example (https://www.example.com)" ) in response.content ) - + # get the displayed form parameters params = copy_form(response.context["form"]) + # we post, confirming we want a ticket response = client.post("/login", params) + # as https://www.example.com is a valid service a ticket should be created and the + # user redirected to the service url self.assert_service_ticket(client, response) def test_view_login_get_auth_denied_service(self): """Request a ticket for a not allowed service by an authenticated client""" + # get a client that is already authenticated client = get_auth_client() + # we are authenticated and are asking for a ticket for https://www.example.org + # which is NOT a valid service response = client.get("/login?service=https://www.example.org") self.assertEqual(response.status_code, 200) + # we warn the user that https://www.example.net is not an allowed service url + # NO ticket are created self.assertTrue(b"Service https://www.example.org non allowed" in response.content) def test_user_logged_not_in_db(self): """If the user is logged but has been delete from the database, it should be logged out""" + # get a client that is already authenticated client = get_auth_client() + # delete the user in the db models.User.objects.get( username=settings.CAS_TEST_USER, session_key=client.session.session_key ).delete() + # fetch the login page response = client.get("/login") + # The user should be logged out self.assert_login_failed(client, response, code=302) + # and redirected to the login page. We branch depending on the version a django as + # the test client behaviour changed after django 1.9 if django.VERSION < (1, 9): # pragma: no cover coverage is computed with dango 1.9 self.assertEqual(response["Location"], "http://testserver/login") else: self.assertEqual(response["Location"], "/login?") def test_service_restrict_user(self): - """Testing the restric user capability fro a service""" + """Testing the restric user capability from a service""" + # get a client that is already authenticated client = get_auth_client() + # trying to get a ticket from a service url matched by a service pattern having a + # restriction on the usernames allowed to get tickets. the test user username is not one + # of this username. response = client.get("/login", {'service': self.service_restrict_user_fail}) self.assertEqual(response.status_code, 200) + # the ticket is not created and a warning is displayed to the user self.assertTrue(b"Username non allowed" in response.content) + # same but with the tes user username being one of the allowed usernames response = client.get("/login", {'service': self.service_restrict_user_success}) + # the ticket is created and we are redirected to the service url self.assertEqual(response.status_code, 302) self.assertTrue( response["Location"].startswith("%s?ticket=" % self.service_restrict_user_success) @@ -238,26 +325,40 @@ class LoginTestCase(TestCase, BaseServicePattern): def test_service_filter(self): """Test the filtering on user attributes""" + # get a client that is already authenticated client = get_auth_client() + # trying to get a ticket from a service url matched by a service pattern having + # a restriction on the user attributes. The test user if ailing these restrictions + # We try first with a single value attribut (aka a string) and then with + # a multi values attributs (aka a list of strings) for service in [self.service_filter_fail, self.service_filter_fail_alt]: response = client.get("/login", {'service': service}) + # the ticket is not created and a warning is displayed to the user self.assertEqual(response.status_code, 200) self.assertTrue(b"User charateristics non allowed" in response.content) + # same but with rectriction that a valid upon the test user attributes response = client.get("/login", {'service': self.service_filter_success}) + # the ticket us created and the user redirected to the service url self.assertEqual(response.status_code, 302) self.assertTrue(response["Location"].startswith("%s?ticket=" % self.service_filter_success)) def test_service_user_field(self): """Test using a user attribute as username: case on if the attribute exists or not""" + # get a client that is already authenticated client = get_auth_client() + # trying to get a ticket from a service url matched by a service pattern that use + # a particular attribute has username. The test user do NOT have this attribute response = client.get("/login", {'service': self.service_field_needed_fail}) + # the ticket is not created and a warning is displayed to the user self.assertEqual(response.status_code, 200) self.assertTrue(b"The attribut uid is needed to use that service" in response.content) + # same but with a attribute that the test user has response = client.get("/login", {'service': self.service_field_needed_success}) + # the ticket us created and the user redirected to the service url self.assertEqual(response.status_code, 302) self.assertTrue( response["Location"].startswith("%s?ticket=" % self.service_field_needed_success) @@ -269,8 +370,13 @@ class LoginTestCase(TestCase, BaseServicePattern): Test using a user attribute as username: case the attribute exists but evaluate to False """ + # get a client that is already authenticated client = get_auth_client() + # trying to get a ticket from a service url matched by a service pattern that use + # a particular attribute has username. The test user have this attribute, but it is + # evaluated to False (eg an empty string "" or an empty list []) response = client.get("/login", {"service": self.service_field_needed_success}) + # the ticket is not created and a warning is displayed to the user self.assertEqual(response.status_code, 200) self.assertTrue(b"The attribut alias is needed to use that service" in response.content) @@ -279,23 +385,31 @@ class LoginTestCase(TestCase, BaseServicePattern): # First with an authenticated client that fail to get a ticket for a service service = "https://restrict_user_fail.example.com" + # get a client that is already authenticated client = get_auth_client() + # the authenticated client fail to get a ticket for some reason response = client.get("/login", {'service': service, 'gateway': 'on'}) + # as gateway is set, he is redirected to the service url without any ticket self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], service) # second for an user not yet authenticated on a valid service client = Client() + # the client fail to get a ticket since he is not yep authenticated response = client.get('/login', {'service': service, 'gateway': 'on'}) + # as gateway is set, he is redirected to the service url without any ticket self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], service) def test_renew(self): """test the authentication renewal request from a service""" + # use the default test service service = "https://www.example.com" + # get a client that is already authenticated client = get_auth_client() + # ask for a ticket for the service but aks for authentication renewal response = client.get("/login", {'service': service, 'renew': 'on'}) - # we are ask to reauthenticate + # we are ask to reauthenticate and tell the user why self.assertEqual(response.status_code, 200) self.assertTrue( ( @@ -303,67 +417,127 @@ class LoginTestCase(TestCase, BaseServicePattern): b"service example (https://www.example.com)" ) in response.content ) + # get the form default parameter params = copy_form(response.context["form"]) + # set valid username/password params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD + # the renew parameter from the form should be True self.assertEqual(params["renew"], True) + # post the authentication request response = client.post("/login", params) + # the request succed, a ticket is created and we are redirected to the service url self.assertEqual(response.status_code, 302) ticket_value = response['Location'].split('ticket=')[-1] ticket = models.ServiceTicket.objects.get(value=ticket_value) - # the created ticket is marked has being gottent after a renew + # the created ticket is marked has being gottent after a renew. Futher testing about + # renewing authentication is done in the validate and serviceValidate views tests self.assertEqual(ticket.renew, True) + @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_login_required(self): - """test ajax, login required""" + """ + test ajax, login required. + The ajax methods allow the log a user in using javascript. + For doing so, every 302 redirection a replaced by a 200 returning a json with the + url to redirect to. + By default, ajax login is disabled. + If CAS_ENABLE_AJAX_AUTH is True, ajax login is enable and only page on the same domain + as the CAS can do ajax request. To allow pages on other domains, you need to use CORS. + You can use the django app corsheaders for that. Be carefull to only allow domains + you completly trust as any javascript on these domaine will be able to authenticate + as the user. + """ + # get a bare client client = Client() + # fetch the login page setting up the custom header HTTP_X_AJAX to tell we wish to de + # ajax requests response = client.get("/login", HTTP_X_AJAX='on') + # we get a json as response telling us the user need to be authenticated self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "error") self.assertEqual(data["detail"], "login required") self.assertEqual(data["url"], "/login?") + @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_logged_user_deleted(self): """test ajax user logged deleted: login required""" + # get a client that is already authenticated client = get_auth_client() + # delete the user in the db user = models.User.objects.get( username=settings.CAS_TEST_USER, session_key=client.session.session_key ) user.delete() + # fetch the login page with ajax on response = client.get("/login", HTTP_X_AJAX='on') + # we get a json telling us that the user need to authenticate self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "error") self.assertEqual(data["detail"], "login required") self.assertEqual(data["url"], "/login?") + @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_logged(self): """test ajax user is successfully logged""" + # get a client that is already authenticated client = get_auth_client() + # fetch the login page with ajax on response = client.get("/login", HTTP_X_AJAX='on') + # we get a json telling us that the user is well authenticated self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") self.assertEqual(data["detail"], "logged") + @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_get_ticket_success(self): """test ajax retrieve a ticket for an allowed service""" + # using the default test service service = "https://www.example.com" + # get a client that is already authenticated client = get_auth_client() + # fetch the login page with ajax on response = client.get("/login", {'service': service}, HTTP_X_AJAX='on') + # we get a json telling us that the ticket has being created + # and we get the url to fetch to authenticate the user to the service + # contening the ticket has GET parameter self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") self.assertEqual(data["detail"], "auth") self.assertTrue(data["url"].startswith('%s?ticket=' % service)) + def test_ajax_get_ticket_success_alt(self): + """ + test ajax retrieve a ticket for an allowed service. + Same as above but with CAS_ENABLE_AJAX_AUTH=False + """ + # using the default test service + service = "https://www.example.com" + # get a client that is already authenticated + client = get_auth_client() + # fetch the login page with ajax on + response = client.get("/login", {'service': service}, HTTP_X_AJAX='on') + # as CAS_ENABLE_AJAX_AUTH is False the ajax request is ignored and word normally: + # 302 redirect to the service url with ticket as GET parameter. javascript + # cannot retieve the ticket info and try follow the redirect to an other domain and fail + # silently + self.assertEqual(response.status_code, 302) + + @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_get_ticket_fail(self): """test ajax retrieve a ticket for a denied service""" + # using a denied service url service = "https://www.example.org" + # get a client that is already authenticated client = get_auth_client() + # fetch the login page with ajax on response = client.get("/login", {'service': service}, HTTP_X_AJAX='on') + # we get a json telling us that the service is not allowed self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "error") @@ -374,11 +548,17 @@ class LoginTestCase(TestCase, BaseServicePattern): "Service https://www.example.org non allowed." ) + @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_get_ticket_warn(self): """test get a ticket but user asked to be warned""" + # using the default test service service = "https://www.example.com" + # get a client that is already authenticated wth warn on client = get_auth_client(warn="on") + # fetch the login page with ajax on response = client.get("/login", {'service': service}, HTTP_X_AJAX='on') + # we get a json telling us that we cannot get a ticket transparently and that the + # user has asked to be warned self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "error") @@ -390,28 +570,36 @@ class LogoutTestCase(TestCase): """test fot the logout view""" def setUp(self): """Prepare the test context""" + # for testing SingleLogOut we need to use a service on localhost were we lanch + # a simple one request http server self.service = 'http://127.0.0.1:45678' self.service_pattern = models.ServicePattern.objects.create( name="localhost", pattern="^https?://127\.0\.0\.1(:[0-9]+)?(/.*)?$", single_log_out=True ) + # return all user attributes models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) def test_logout(self): """logout is idempotent""" + # get a bare client client = Client() + # call logout client.get("/logout") + # we are still not logged self.assertFalse(client.session.get("username")) self.assertFalse(client.session.get("authenticated")) def test_logout_view(self): - """test simple logout""" + """test simple logout, logout only an user from one and only one sessions""" + # get two authenticated client with the same test user (but two different sessions) client = get_auth_client() client2 = get_auth_client() + # fetch login, the first client is well authenticated response = client.get("/login") self.assertEqual(response.status_code, 200) self.assertTrue( @@ -420,10 +608,13 @@ class LogoutTestCase(TestCase): b"the Central Authentication Service" ) in response.content ) + # and session variable are well self.assertTrue(client.session["username"] == settings.CAS_TEST_USER) self.assertTrue(client.session["authenticated"] is True) + # call logout with the first client response = client.get("/logout") + # the client is logged out self.assertEqual(response.status_code, 200) self.assertTrue( ( @@ -431,7 +622,7 @@ class LogoutTestCase(TestCase): b"the Central Authentication Service" ) in response.content ) - + # and session variable a well cleaned self.assertFalse(client.session.get("username")) self.assertFalse(client.session.get("authenticated")) # client2 is still logged @@ -439,6 +630,7 @@ class LogoutTestCase(TestCase): self.assertTrue(client2.session["authenticated"] is True) response = client.get("/login") + # fetch login, the second client is well authenticated self.assertEqual(response.status_code, 200) self.assertFalse( ( @@ -449,9 +641,11 @@ class LogoutTestCase(TestCase): def test_logout_from_all_session(self): """test logout from all my session""" + # get two authenticated client with the same test user (but two different sessions) client = get_auth_client() client2 = get_auth_client() + # call logout with the first client and ask to be logged out from all of this user sessions client.get("/logout?all=1") # both client are logged out @@ -462,12 +656,14 @@ class LogoutTestCase(TestCase): def assert_redirect_to_service(self, client, response): """assert logout redirect to parameter""" + # assert a redirection with a service self.assertEqual(response.status_code, 302) self.assertTrue(response.has_header("Location")) self.assertEqual(response["Location"], "https://www.example.com") response = client.get("/login") self.assertEqual(response.status_code, 200) + # assert we are not longer logged in self.assertFalse( ( b"You have successfully logged into " @@ -477,16 +673,22 @@ class LogoutTestCase(TestCase): def test_logout_view_url(self): """test logout redirect to url parameter""" + # get a client that is authenticated client = get_auth_client() + # logout with an url paramer response = client.get('/logout?url=https://www.example.com') + # we are redirected to the addresse of the url parameter self.assert_redirect_to_service(client, response) def test_logout_view_service(self): """test logout redirect to service parameter""" + # get a client that is authenticated client = get_auth_client() + # logout with a service parameter response = client.get('/logout?service=https://www.example.com') + # we are redirected to the addresse of the service parameter self.assert_redirect_to_service(client, response) def test_logout_slo(self): @@ -494,30 +696,47 @@ class LogoutTestCase(TestCase): parameters = [] # test normal SLO + # setup a simple one request http server (httpd, host, port) = utils.HttpParamsHandler.run()[0:3] + # build a service url depending on which port the http server has binded service = "http://%s:%s" % (host, port) + # get a ticket requested by client and being validated by the service (client, ticket) = get_validated_ticket(service)[:2] + # the client logout triggering the send of the SLO requests client.get('/logout') + # we store the POST parameters send for this ticket for furthur analisys parameters.append((httpd.PARAMS, ticket)) # text SLO with a single_log_out_callback + # setup a simple one request http server (httpd, host, port) = utils.HttpParamsHandler.run()[0:3] + # set the default test service pattern to use the http server port for SLO requests. + # in fact, this single_log_out_callback parametter is usefull to implement SLO + # for non http service like imap or ftp self.service_pattern.single_log_out_callback = "http://%s:%s" % (host, port) self.service_pattern.save() + # get a ticket requested by client and being validated by the service (client, ticket) = get_validated_ticket(self.service)[:2] + # the client logout triggering the send of the SLO requests client.get('/logout') + # we store the POST parameters send for this ticket for furthur analisys parameters.append((httpd.PARAMS, ticket)) + # for earch POST parameters and corresponding ticket for (params, ticket) in parameters: + # there is a POST parameter 'logoutRequest' self.assertTrue(b'logoutRequest' in params and params[b'logoutRequest']) + # it is a valid xml root = etree.fromstring(params[b'logoutRequest'][0]) + # contening a tag self.assertTrue( root.xpath( "//samlp:LogoutRequest", namespaces={"samlp": "urn:oasis:names:tc:SAML:2.0:protocol"} ) ) + # with a tag enclosing the value of the ticket session_index = root.xpath( "//samlp:SessionIndex", namespaces={"samlp": "urn:oasis:names:tc:SAML:2.0:protocol"} @@ -527,25 +746,40 @@ class LogoutTestCase(TestCase): # SLO error are displayed on logout page (client, ticket) = get_validated_ticket(self.service)[:2] + # the client logout triggering the send of the SLO requests but + # not http server are listening response = client.get('/logout') self.assertTrue(b"Error during service logout" in response.content) + @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_logout(self): - """test ajax logout""" + """ + test ajax logout. These methode are here, but I do not really see an use case for + javascript logout + """ + # get a client that is authenticated client = get_auth_client() + # fetch the logout page with ajax on response = client.get('/logout', HTTP_X_AJAX='on') + # we get a json telling us the user is well logged out self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") self.assertEqual(data["detail"], "logout") self.assertEqual(data['session_nb'], 1) + @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_logout_all_session(self): """test ajax logout from a random number a sessions""" + # fire a random int in [2, 10[ nb_client = random.randint(2, 10) + # get this much of logged clients all for the test user clients = [get_auth_client() for i in range(nb_client)] + # fetch the logout page with ajax on, requesting to logout from all sessions response = clients[0].get('/logout?all=1', HTTP_X_AJAX='on') + # we get a json telling us the user is well logged out and the number of session + # the user has being logged out self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") @@ -555,9 +789,12 @@ class LogoutTestCase(TestCase): @override_settings(CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT=True) def test_redirect_after_logout(self): """Test redirect to login after logout parameter""" + # get a client that is authenticated client = get_auth_client() + # fetch the logout page response = client.get('/logout') + # as CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT is True, we are redirected to the login page self.assertEqual(response.status_code, 302) if django.VERSION < (1, 9): # pragma: no cover coverage is computed with dango 1.9 self.assertEqual(response["Location"], "http://testserver/login") @@ -569,20 +806,29 @@ class LogoutTestCase(TestCase): @override_settings(CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT=True) def test_redirect_after_logout_to_service(self): """test prevalence of redirect url/service parameter over redirect to login after logout""" + # get a client that is authenticated client = get_auth_client() + # fetch the logout page with an url parameter response = client.get('/logout?url=https://www.example.com') + # we are redirected to the url parameter and not to the login page self.assert_redirect_to_service(client, response) + # fetch the logout page with an service parameter response = client.get('/logout?service=https://www.example.com') + # we are redirected to the service parameter and not to the login page self.assert_redirect_to_service(client, response) - @override_settings(CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT=True) + @override_settings(CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT=True, CAS_ENABLE_AJAX_AUTH=True) def test_ajax_redirect_after_logout(self): """Test ajax redirect to login after logout parameter""" + # get a client that is authenticated client = get_auth_client() + # fetch the logout page with ajax on response = client.get('/logout', HTTP_X_AJAX='on') + # we get a json telling us the user is well logged out. And url key is added to aks for + # redirection to the login page self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") @@ -599,6 +845,7 @@ class AuthTestCase(TestCase): """ def setUp(self): """preparing test context""" + # setting up a default test service url and pattern self.service = 'https://www.example.com' models.ServicePattern.objects.create( name="example", @@ -608,7 +855,11 @@ class AuthTestCase(TestCase): @override_settings(CAS_AUTH_SHARED_SECRET='test') def test_auth_view_goodpass(self): """successful request are awsered by yes""" - client = get_auth_client() + # get a bare client + client = Client() + # post the the auth view a valid (username, password, service) and the shared secret + # to test the user again the service, a user is created in the database for the + # current session and is then deleted as the user is not authenticated response = client.post( '/auth', { @@ -618,13 +869,19 @@ class AuthTestCase(TestCase): 'secret': 'test' } ) + # as (username, password, service) and the hared secret are valid, we get yes as a response self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'yes\n') @override_settings(CAS_AUTH_SHARED_SECRET='test') def test_auth_view_goodpass_logged(self): """successful request are awsered by yes, using a logged sessions""" - client = Client() + # same as above + client = get_auth_client() + # to test the user again the service, a user is fetch in the database for the + # current session and is NOT deleted as the user is currently logged. + # Deleting the user from the database would cause the user to be logged out as + # showed in the login tests response = client.post( '/auth', { @@ -634,6 +891,7 @@ class AuthTestCase(TestCase): 'secret': 'test' } ) + # as (username, password, service) and the hared secret are valid, we get yes as a response self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'yes\n') @@ -723,31 +981,39 @@ class ValidateTestCase(TestCase): """tests for the validate view""" def setUp(self): """preparing test context""" + # setting up a default test service url and pattern self.service = 'https://www.example.com' self.service_pattern = models.ServicePattern.objects.create( name="example", pattern="^https://www\.example\.com(/.*)?$" ) + models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) + # setting up a test service and pattern using a multi valued user attribut as username + # the first value of the list should be used as username self.service_user_field = "https://user_field.example.com" self.service_pattern_user_field = models.ServicePattern.objects.create( name="user field", pattern="^https://user_field\.example\.com(/.*)?$", user_field="alias" ) + # setting up a test service and pattern using a single valued user attribut as username self.service_user_field_alt = "https://user_field_alt.example.com" self.service_pattern_user_field_alt = models.ServicePattern.objects.create( name="user field alt", pattern="^https://user_field_alt\.example\.com(/.*)?$", user_field="nom" ) - models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) def test_validate_view_ok(self): """test for a valid (ticket, service)""" + # get a ticket waiting to be validated for self.service ticket = get_user_ticket_request(self.service)[1] + # get a bare client client = Client() + # calling the validate view with this ticket value and service response = client.get('/validate', {'ticket': ticket.value, 'service': self.service}) + # get yes as a response and the test user username self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'yes\ntest\n') @@ -756,10 +1022,12 @@ class ValidateTestCase(TestCase): ticket = get_user_ticket_request(self.service)[1] client = Client() + # calling the validate view with this ticket value and another service response = client.get( '/validate', {'ticket': ticket.value, 'service': "https://www.example.org"} ) + # the ticket service and validation service do not match, validation should fail self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') @@ -768,10 +1036,12 @@ class ValidateTestCase(TestCase): get_user_ticket_request(self.service) client = Client() + # calling the validate view with another ticket value and this service response = client.get( '/validate', {'ticket': "%s-RANDOM" % settings.CAS_SERVICE_TICKET_PREFIX, 'service': self.service} ) + # as the ticket is bad, validation should fail self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') @@ -791,6 +1061,7 @@ class ValidateTestCase(TestCase): {'ticket': ticket.value, 'service': service} ) self.assertEqual(response.status_code, 200) + # the user attribute is well used as username self.assertEqual(response.content, b'yes\n' + username + b'\n') def test_validate_missing_parameter(self): @@ -803,6 +1074,8 @@ class ValidateTestCase(TestCase): send_params = params.copy() del send_params[key] response = client.get('/validate', send_params) + # if the GET request is missing the ticket or + # service GET parameter, validation should fail self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') @@ -812,20 +1085,27 @@ class ValidateServiceTestCase(TestCase, XmlContent): """tests for the serviceValidate view""" def setUp(self): """preparing test context""" + # for testing SingleLogOut and Proxy GrantingTicket transmission + # we need to use a service on localhost were we launch + # a simple one request http server self.service = 'http://127.0.0.1:45678' self.service_pattern = models.ServicePattern.objects.create( name="localhost", pattern="^https?://127\.0\.0\.1(:[0-9]+)?(/.*)?$", + # allow to request PGT by the service proxy_callback=True ) + # tell the service pattern to transmit all the user attributes (* is a joker) models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) + # test service pattern using the attribute alias as username self.service_user_field = "https://user_field.example.com" self.service_pattern_user_field = models.ServicePattern.objects.create( name="user field", pattern="^https://user_field\.example\.com(/.*)?$", user_field="alias" ) + # test service pattern using the attribute nom as username self.service_user_field_alt = "https://user_field_alt.example.com" self.service_pattern_user_field_alt = models.ServicePattern.objects.create( name="user field alt", @@ -833,6 +1113,7 @@ class ValidateServiceTestCase(TestCase, XmlContent): user_field="nom" ) + # test service pattern only transmiting one single attributes self.service_one_attribute = "https://one_attribute.example.com" self.service_pattern_one_attribute = models.ServicePattern.objects.create( name="one_attribute", @@ -843,6 +1124,7 @@ class ValidateServiceTestCase(TestCase, XmlContent): service_pattern=self.service_pattern_one_attribute ) + # test service pattern testing attribute name and value replacement self.service_replace_attribute_list = "https://replace_attribute_list.example.com" self.service_pattern_replace_attribute_list = models.ServicePattern.objects.create( name="replace_attribute_list", @@ -878,10 +1160,15 @@ class ValidateServiceTestCase(TestCase, XmlContent): def test_validate_service_view_ok(self): """test with a valid (ticket, service), the username and all attributes are transmited""" + # get a ticket from an authenticated user waiting for validation ticket = get_user_ticket_request(self.service)[1] + # get a bare client client = Client() + # requesting validation with a good (ticket, service) response = client.get('/serviceValidate', {'ticket': ticket.value, 'service': self.service}) + # the validation should succes with username settings.CAS_TEST_USER and transmit + # the attributes settings.CAS_TEST_ATTRIBUTES self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES) def test_validate_service_view_ok_one_attribute(self): @@ -889,6 +1176,7 @@ class ValidateServiceTestCase(TestCase, XmlContent): test with a valid (ticket, service), the username and the 'nom' only attribute are transmited """ + # get a ticket for a service that transmit only one attribute ticket = get_user_ticket_request(self.service_one_attribute)[1] client = Client() @@ -896,6 +1184,8 @@ class ValidateServiceTestCase(TestCase, XmlContent): '/serviceValidate', {'ticket': ticket.value, 'service': self.service_one_attribute} ) + # the validation should succed, returning settings.CAS_TEST_USER as username and a single + # attribute 'nom' self.assert_success( response, settings.CAS_TEST_USER, @@ -904,6 +1194,8 @@ class ValidateServiceTestCase(TestCase, XmlContent): def test_validate_replace_attributes(self): """test with a valid (ticket, service), attributes name and value replacement""" + # get a ticket for a service pattern replacing attributes names + # nom -> NOM and value nom -> s/^N/P/ for a single valued attribute ticket = get_user_ticket_request(self.service_replace_attribute)[1] client = Client() response = client.get( @@ -916,6 +1208,8 @@ class ValidateServiceTestCase(TestCase, XmlContent): {'NOM': 'Pymous'} ) + # get a ticket for a service pattern replacing attributes names + # alias -> ALIAS and value alias -> s/demo/truc/ for a multi valued attribute ticket = get_user_ticket_request(self.service_replace_attribute_list)[1] client = Client() response = client.get( @@ -930,11 +1224,14 @@ class ValidateServiceTestCase(TestCase, XmlContent): def test_validate_service_view_badservice(self): """test with a valid ticket but a bad service, the validatin should fail""" + # get a ticket for service A ticket = get_user_ticket_request(self.service)[1] client = Client() bad_service = "https://www.example.org" + # try to validate it for service B response = client.get('/serviceValidate', {'ticket': ticket.value, 'service': bad_service}) + # the validation should fail with error code "INVALID_SERVICE" self.assert_error( response, "INVALID_SERVICE", @@ -943,7 +1240,7 @@ class ValidateServiceTestCase(TestCase, XmlContent): def test_validate_service_view_badticket_goodprefix(self): """ - test with a good service bud a bad ticket begining with ST-, + test with a good service but a bad ticket begining with ST-, the validation should fail with the error (INVALID_TICKET, ticket not found) """ get_user_ticket_request(self.service) @@ -975,31 +1272,41 @@ class ValidateServiceTestCase(TestCase, XmlContent): def test_validate_service_view_ok_pgturl(self): """test the retrieval of a ProxyGrantingTicket""" + # start a simple on request http server (httpd, host, port) = utils.HttpParamsHandler.run()[0:3] + # construct the service from it service = "http://%s:%s" % (host, port) + # get a ticket to be validated ticket = get_user_ticket_request(service)[1] client = Client() + # request a PGT ticket then validating the ticket by setting the pgtUrl parameter response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': service, 'pgtUrl': service} ) + # We should have recieved the PGT via a GET request parameter on the simple http server pgt_params = httpd.PARAMS self.assertEqual(response.status_code, 200) root = etree.fromstring(response.content) + # the validation response should return a id to match again the request transmitting the PGT pgtiou = root.xpath( "//cas:proxyGrantingTicket", namespaces={'cas': "http://www.yale.edu/tp/cas"} ) self.assertEqual(len(pgtiou), 1) + # the matching id for making corresponde one PGT to a validatin response should match self.assertEqual(pgt_params["pgtIou"], pgtiou[0].text) + # the PGT is present in the receive GET requests parameters self.assertTrue("pgtId" in pgt_params) def test_validate_service_pgturl_sslerror(self): """test the retrieval of a ProxyGrantingTicket with a SSL error on the pgtUrl""" (host, port) = utils.HttpParamsHandler.run()[1:3] + # is fact the service listen on http and not https raisin a SSL Protocol Error + # but other SSL/TLS error should behave the same service = "https://%s:%s" % (host, port) ticket = get_user_ticket_request(service)[1] @@ -1009,6 +1316,9 @@ class ValidateServiceTestCase(TestCase, XmlContent): '/serviceValidate', {'ticket': ticket.value, 'service': service, 'pgtUrl': service} ) + # The pgtUrl is validated: it must be localhost or have valid x509 certificat and + # certificat validation should succed. Moreother, pgtUrl should match a service pattern + # with proxy_callback set to True self.assert_error( response, "INVALID_PROXY_CALLBACK", @@ -1028,7 +1338,9 @@ class ValidateServiceTestCase(TestCase, XmlContent): '/serviceValidate', {'ticket': ticket.value, 'service': service, 'pgtUrl': service} ) + # The ticket is successfully validated root = self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES) + # but no PGT is transmitted pgtiou = root.xpath( "//cas:proxyGrantingTicket", namespaces={'cas': "http://www.yale.edu/tp/cas"} @@ -1074,12 +1386,15 @@ class ValidateServiceTestCase(TestCase, XmlContent): (self.service_user_field, settings.CAS_TEST_ATTRIBUTES["alias"][0]), (self.service_user_field_alt, settings.CAS_TEST_ATTRIBUTES["nom"]) ]: + # requesting a ticket for a service url matched by a service pattern using a user + # attribute as username ticket = get_user_ticket_request(service)[1] client = Client() response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': service} ) + # The validate shoudl be successful with specified username and no attributes transmited self.assert_success( response, username, @@ -1096,6 +1411,7 @@ class ValidateServiceTestCase(TestCase, XmlContent): send_params = params.copy() del send_params[key] response = client.get('/serviceValidate', send_params) + # a validation request with a missing GET parameter should fail self.assert_error( response, "INVALID_REQUEST",