def test_auth(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping authentication tests") return r = client.get(f"{API_URI}/tests/authentication") assert r.status_code == 401 r = client.get( f"{API_URI}/tests/authentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 headers, token = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/authentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == BaseAuthentication.default_user if not Env.get_bool("ALLOW_ACCESS_TOKEN_PARAMETER"): # access token parameter is not allowed by default r = client.get(f"{API_URI}/tests/authentication", query_string={"access_token": token}) assert r.status_code == 401
def test_05_login_failures(self, client: FlaskClient) -> None: if Env.get_bool("MAIN_LOGIN_ENABLE") and Env.get_bool("AUTH_ENABLE"): # Create a new user on the fly to test the cached endpoint _, data = self.create_user(client) headers, _ = self.do_login( client, data["email"], data["password"], test_failures=True ) r = client.get(f"{AUTH_URI}/logout", headers=headers) assert r.status_code == 204
def test_auth(self, client: FlaskClient) -> None: r = client.get(f"{API_URI}/tests/authentication") assert r.status_code == 401 r = client.get( f"{API_URI}/tests/authentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 headers, token = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/authentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == token assert content["user"] == BaseAuthentication.default_user if not Env.get_bool("ALLOW_ACCESS_TOKEN_PARAMETER"): # access token parameter is not allowed by default r = client.get( f"{API_URI}/tests/authentication", query_string={"access_token": token} ) assert r.status_code == 401
def create_user( cls, client: FlaskClient, data: Optional[Dict[str, Any]] = None, roles: Optional[List[Union[str, Role]]] = None, ) -> Tuple[str, Dict[str, Any]]: assert Env.get_bool("MAIN_LOGIN_ENABLE") admin_headers, _ = cls.do_login(client, None, None) assert admin_headers is not None schema = cls.getDynamicInputSchema(client, "admin/users", admin_headers) user_data = cls.buildData(schema) if Connector.check_availability("smtp"): user_data["email_notification"] = False user_data["is_active"] = True user_data["expiration"] = None if roles: for idx, role in enumerate(roles): if isinstance(role, Role): roles[idx] = role.value user_data["roles"] = json.dumps(roles) if data: user_data.update(data) r = client.post(f"{API_URI}/admin/users", data=user_data, headers=admin_headers) assert r.status_code == 200 uuid = cls.get_content(r) return uuid, user_data
def test_01_login_ban_not_enabled(self, client: FlaskClient) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Skipping admin/users tests") return uuid, data = self.create_user(client) # Login attempts are not registered, let's try to fail the login many times for _ in range(0, 10): self.do_login(client, data["email"], "wrong", status_code=401) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == data["email"] # and verify that login is still allowed headers, _ = self.do_login(client, data["email"], data["password"]) assert headers is not None events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == data["email"] # Goodbye temporary user self.delete_user(client, uuid)
def test_05_no_notification_email_for_wrong_usernames( self, client: FlaskClient, faker: Faker ) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Skipping admin/users tests") return uuid, data = self.create_user(client) self.delete_mock_email() # Just to verify that email is deleted with pytest.raises(FileNotFoundError): self.read_mock_email() email = faker.ascii_email() # Wrong credentials with a non existing email # -> No notification will be sent for _ in range(0, max_login_attempts): self.do_login(client, email, data["password"], status_code=401) # Verify the ban (i.e. status 403) headers, _ = self.do_login(client, email, data["password"], status_code=403) assert headers is None # Verify that there are no mocked email with pytest.raises(FileNotFoundError): self.read_mock_email() # Goodbye temporary user self.delete_user(client, uuid)
def test_04_logout(self, client: FlaskClient) -> None: """Check that you can logout with a valid token""" if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping logout tests") return # Check success log.info("*** VERIFY valid token") r = client.get(f"{AUTH_URI}/logout", headers=self.get("auth_header")) assert r.status_code == 204 events = self.get_last_events(2) assert events[0].event == Events.delete.value assert events[0].user == "-" assert events[0].target_type == "Token" assert events[0].url == "/auth/logout" assert events[1].event == Events.logout.value assert events[1].user == BaseAuthentication.default_user assert events[1].url == "/auth/logout" # Check failure log.info("*** VERIFY invalid token") r = client.get(f"{AUTH_URI}/logout") assert r.status_code == 401
def test_GET_status(self, client: FlaskClient) -> None: """Test that the flask server is running and reachable""" # Check success alive_message = "Server is alive" log.info("*** VERIFY if API is online") r = client.get(f"{API_URI}/status") assert r.status_code == 200 output = self.get_content(r) assert output == alive_message # Check failure log.info("*** VERIFY if invalid endpoint gives Not Found") r = client.get(API_URI) assert r.status_code == 404 if Env.get_bool("AUTH_ENABLE"): # Check /auth/status with no token or invalid token r = client.get(f"{AUTH_URI}/status") assert r.status_code == 401 r = client.get(f"{AUTH_URI}/status", headers={"Authorization": "Bearer ABC"}) assert r.status_code == 401 else: r = client.get(f"{AUTH_URI}/status") assert r.status_code == 404
def test_authentication_with_multiple_roles(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping authentication tests") return r = client.get(f"{API_URI}/tests/manyrolesauthentication") assert r.status_code == 401 r = client.get(f"{API_URI}/tests/unknownroleauthentication") assert r.status_code == 401 admin_headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/manyrolesauthentication", headers=admin_headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == BaseAuthentication.default_user r = client.get(f"{API_URI}/tests/unknownroleauthentication", headers=admin_headers) assert r.status_code == 401 if Env.get_bool("MAIN_LOGIN_ENABLE"): uuid, data = self.create_user(client, roles=[Role.USER]) user_header, _ = self.do_login(client, data.get("email"), data.get("password")) r = client.get(f"{API_URI}/tests/manyrolesauthentication", headers=user_header) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == data.get("email") r = client.get(f"{API_URI}/tests/unknownroleauthentication", headers=user_header) assert r.status_code == 401 self.delete_user(client, uuid)
def test_cached_authenticated_param_endpoint(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping cache with authentication tests") return headers1, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/cache/paramauth", headers=headers1) assert r.status_code == 200 resp1 = self.get_content(r) assert isinstance(resp1, list) # counter is 1 because this is the first request to this endpoint assert resp1[COUNTER] == 1 r = client.get(f"{API_URI}/tests/cache/paramauth", headers=headers1) assert r.status_code == 200 resp2 = self.get_content(r) assert isinstance(resp2, list) assert resp2[UUID] == resp1[UUID] # Same counter as above, because the response is replied from the cache assert resp2[COUNTER] == resp1[COUNTER] assert resp2[COUNTER] == 1 headers2, token2 = self.do_login(client, None, None) # Test by using access_token parameter instead of Headers r = client.get(f"{API_URI}/tests/cache/paramauth", query_string={"access_token": token2}) assert r.status_code == 200 resp3 = self.get_content(r) assert isinstance(resp3, list) # This is the same user, uuid is unchanged assert resp3[UUID] == resp1[UUID] # but counter changed, because the response is not replied from the cache assert resp3[COUNTER] != resp1[COUNTER] assert resp3[COUNTER] == 2 r = client.get(f"{API_URI}/tests/cache/paramauth", query_string={"access_token": token2}) assert r.status_code == 200 resp4 = self.get_content(r) assert isinstance(resp4, list) assert resp4[UUID] == resp1[UUID] # Same counter as above, because the response is replied from the cache assert resp4[COUNTER] == resp3[COUNTER] assert resp4[COUNTER] == 2 # Cache is stored starting from the access_token parameter, # but the token is the same also if provided as header r = client.get(f"{API_URI}/tests/cache/paramauth", headers=headers2) assert r.status_code == 200 resp5 = self.get_content(r) assert isinstance(resp5, list) assert resp5[UUID] == resp1[UUID] # Same counter as above, because the response is replied from the cache assert resp5[COUNTER] == resp3[COUNTER] assert resp5[COUNTER] == 2
def test_optional_auth(self, client: FlaskClient) -> None: # Optional authentication can accept missing tokens r = client.get(f"{API_URI}/tests/optionalauthentication") assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] is None assert content["user"] is None headers, token = self.do_login(client, None, None) # Or valid tokens r = client.get(f"{API_URI}/tests/optionalauthentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == token assert content["user"] == BaseAuthentication.default_user # But not invalid tokens, i.e. if presented the tokens is always validated r = client.get( f"{API_URI}/tests/authentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 if not Env.get_bool("ALLOW_ACCESS_TOKEN_PARAMETER"): # access token parameter is not allowed by default r = client.get( f"{API_URI}/tests/optionalauthentication", query_string={"access_token": token}, ) # query token is ignored but the endpoint accepts missing tokens assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] is None assert content["user"] is None r = client.get( f"{API_URI}/tests/optionalauthentication", query_string={"access_token": "invalid"}, ) # invalid tokens should be rejected, but query token is ignored assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] is None assert content["user"] is None
def test_caching_general_clearing(self, client: FlaskClient) -> None: if Env.get_bool("AUTH_ENABLE"): headers, _ = self.do_login(client, None, None) else: headers = None # get method is cached for 200 seconds # First response is not cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 counter1 = self.get_content(r) # Second response is cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 assert self.get_content(r) == counter1 # Empty all the cache Cache.clear() # Third response is no longer cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 counter2 = self.get_content(r) assert counter2 != counter1 # Response is still cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 assert self.get_content(r) == counter2 # Empty the endpoint cache client.delete(f"{API_URI}/tests/cache/long") # Second response is no longer cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 counter3 = self.get_content(r) assert counter3 != counter2 # Response is still cached r = client.get(f"{API_URI}/tests/cache/long") assert r.status_code == 200 assert self.get_content(r) == counter3 # Endpoint is unauthenticated, headers are ignored when building the cache key r = client.get(f"{API_URI}/tests/cache/long", headers=headers) assert r.status_code == 200 assert self.get_content(r) == counter3 # Tokens are ignored even if invalid r = client.get(f"{API_URI}/tests/cache/long", headers={"Authorization": "Bearer invalid"}) assert r.status_code == 200 assert self.get_content(r) == counter3
def delete_user(cls, client: FlaskClient, uuid: str) -> None: assert Env.get_bool("MAIN_LOGIN_ENABLE") admin_headers, _ = cls.do_login(client, None, None) assert admin_headers is not None r = client.delete(f"{API_URI}/admin/users/{uuid}", headers=admin_headers) assert r.status_code == 204
def test_cached_semiauthenticated_endpoint(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping cache with authentication tests") return r = client.get(f"{API_URI}/tests/cache/optionalauth") assert r.status_code == 200 nonauthenticated1 = self.get_content(r) assert isinstance(nonauthenticated1, list) assert nonauthenticated1[UUID] == "N/A" # counter is 1 because this is the first request to this endpoint assert nonauthenticated1[COUNTER] == 1 r = client.get(f"{API_URI}/tests/cache/optionalauth") assert r.status_code == 200 nonauthenticated2 = self.get_content(r) assert isinstance(nonauthenticated2, list) assert nonauthenticated2[UUID] == "N/A" # Same counter as above, because the response is replied from the cache assert nonauthenticated2[COUNTER] == 1 headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/cache/optionalauth", headers=headers) assert r.status_code == 200 authenticated1 = self.get_content(r) assert isinstance(authenticated1, list) assert authenticated1[UUID] != "N/A" # The counter changed, because the response is not replied from the cache assert authenticated1[COUNTER] == 2 # Token cached => cache should be used r = client.get(f"{API_URI}/tests/cache/optionalauth", headers=headers) assert r.status_code == 200 authenticated2 = self.get_content(r) assert isinstance(authenticated2, list) assert authenticated2[UUID] == authenticated1[UUID] # Same counter as above, because the response is replied from the cache assert authenticated2[COUNTER] == 2 # New token => no cache headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/cache/optionalauth", headers=headers) assert r.status_code == 200 authenticated2 = self.get_content(r) assert isinstance(authenticated2, list) assert authenticated2[UUID] == authenticated1[UUID] # Counter changed assert authenticated2[COUNTER] == 3 r = client.get( f"{API_URI}/tests/cache/optionalauth", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401
def test_caching_autocleaning(self, client: FlaskClient) -> None: if Env.get_bool("AUTH_ENABLE"): headers, _ = self.do_login(client, None, None) else: headers = None # Syncronize this test to start at the beginning of the next second and # prevent the test to overlap a change of second # Since the caching is rounded to the second, few milliseconds cann make the # difference, for example: # A first request at 00:00:00.997 is cached # A second request at 00:00:01.002 is no longer cached, even if only 5 millisec # elapsed because the second changed # Added 0.01 just to avoid to exactly start at the beginning of the second t = 1.01 - datetime.now().microsecond / 1000000.0 log.critical("Sleeping {} sec", t) time.sleep(t) # the GET method is cached for 1 second # First response is not cached r = client.get(f"{API_URI}/tests/cache/short") assert r.status_code == 200 counter1 = self.get_content(r) # Second response is cached r = client.get(f"{API_URI}/tests/cache/short") assert r.status_code == 200 assert self.get_content(r) == counter1 # Third response is no longer cached time.sleep(1) r = client.get(f"{API_URI}/tests/cache/short") assert r.status_code == 200 counter2 = self.get_content(r) assert counter2 != counter1 # Fourth response is cached again r = client.get(f"{API_URI}/tests/cache/short") assert r.status_code == 200 assert self.get_content(r) == counter2 # Endpoint is unauthenticated, headers are ignored when building the cache key r = client.get(f"{API_URI}/tests/cache/short", headers=headers) assert r.status_code == 200 assert self.get_content(r) == counter2 # Tokens are ignored even if invalid r = client.get(f"{API_URI}/tests/cache/short", headers={"Authorization": "Bearer invalid"}) assert r.status_code == 200 assert self.get_content(r) == counter2
def test_authentication_with_auth_callback(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping authentication tests") return auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None VALID = f"/tests/preloadcallback/{user.uuid}" INVALID = "/tests/preloadcallback/12345678-90ab-cdef-1234-567890abcdef" admin_headers, _ = self.do_login(client, None, None) # Verify both endpoint ... r = client.get(f"{API_URI}{VALID}", query_string={"test": True}, headers=admin_headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == user.email r = client.get(f"{API_URI}{INVALID}", query_string={"test": True}, headers=admin_headers) assert r.status_code == 401 # and get_schema! r = client.get( f"{API_URI}{VALID}", query_string={"get_schema": True}, headers=admin_headers, ) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 1 assert content[0]["key"] == "test" assert content[0]["type"] == "boolean" r = client.get( f"{API_URI}{INVALID}", query_string={"get_schema": True}, headers=admin_headers, ) assert r.status_code == 401
def post(self, **kwargs: Any) -> Response: """ Register new user """ email = kwargs.get("email") user = self.auth.get_user(username=email) if user is not None: raise Conflict(f"This user already exists: {email}") password_confirm = kwargs.pop("password_confirm") if kwargs.get("password") != password_confirm: raise Conflict("Your password doesn't match the confirmation") if self.auth.VERIFY_PASSWORD_STRENGTH: check, msg = self.auth.verify_password_strength( kwargs.get("password"), None) if not check: raise Conflict(msg) kwargs["is_active"] = False user = self.auth.create_user(kwargs, [self.auth.default_role]) default_group = self.auth.get_group(name=DEFAULT_GROUP_NAME) self.auth.add_user_to_group(user, default_group) self.auth.save_user(user) self.log_event(self.events.create, user, kwargs) try: smtp_client = smtp.get_instance() if Env.get_bool("REGISTRATION_NOTIFICATIONS"): # Sending an email to the administrator title = get_project_configuration("project.title", default="Unkown title") subject = f"{title} New credentials requested" body = f"New credentials request from {user.email}" smtp_client.send(body, subject) send_activation_link(smtp_client, self.auth, user) except BaseException as e: # pragma: no cover self.auth.delete_user(user) raise ServiceUnavailable( f"Errors during account registration: {e}") return self.response( "We are sending an email to your email address where " "you will find the link to activate your account")
def test_database_exceptions(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping dabase exceptions tests") return # This is a special value. The endpoint will try to create a group without # shortname. A BadRequest is expected because the database should refuse the # entry due to the missing property r = client.post(f"{API_URI}/tests/database/400") assert r.status_code == 400 # This is the message of a DatabaseMissingRequiredProperty assert self.get_content(r) == "Missing property shortname required by Group" auth = Connector.get_authentication_instance() default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None # the /tests/database endpoint will change the default group fullname # as a side effect to the test the database_transaction decorator default_fullname = default_group.fullname random_name = faker.pystr() # This will create a new group with short/full name == random_name r = client.post(f"{API_URI}/tests/database/{random_name}") assert r.status_code == 200 default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None # As a side effect the fullname of defaut_group is changed... assert default_group.fullname != default_fullname # ... and this is the new name new_fullname = default_group.fullname # This will try to create again a group with short/full name == random_name # but this will fail due to unique keys r = client.post(f"{API_URI}/tests/database/{random_name}") assert r.status_code == 409 # This is the message of a DatabaseDuplicatedEntry self.get_content(r) == "A Group already exists with 'shortname': '400'" # The default group will not change again because the # database_transaction decorator will undo the change default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None assert default_group.fullname == new_fullname
def test_authentication_with_multiple_roles(self, client: FlaskClient) -> None: r = client.get(f"{API_URI}/tests/manyrolesauthentication") assert r.status_code == 401 r = client.get(f"{API_URI}/tests/unknownroleauthentication") assert r.status_code == 401 admin_headers, admin_token = self.do_login(client, None, None) r = client.get( f"{API_URI}/tests/manyrolesauthentication", headers=admin_headers ) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == admin_token assert content["user"] == BaseAuthentication.default_user r = client.get( f"{API_URI}/tests/unknownroleauthentication", headers=admin_headers ) assert r.status_code == 401 if Env.get_bool("MAIN_LOGIN_ENABLE"): uuid, data = self.create_user(client, roles=[Role.USER]) user_header, user_token = self.do_login( client, data.get("email"), data.get("password") ) r = client.get( f"{API_URI}/tests/manyrolesauthentication", headers=user_header ) assert r.status_code == 200 content = self.get_content(r) assert len(content) == 2 assert "token" in content assert "user" in content assert content["token"] == user_token assert content["user"] == data.get("email") r = client.get( f"{API_URI}/tests/unknownroleauthentication", headers=user_header ) assert r.status_code == 401 self.delete_user(client, uuid)
def test_06_token_ip_validity(self, client: FlaskClient, faker: Faker) -> None: if Env.get_bool("MAIN_LOGIN_ENABLE") and Env.get_bool("AUTH_ENABLE"): if Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD") < 10: headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) assert r.status_code == 200 time.sleep(Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD")) r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) assert r.status_code == 401 # After the failure the token is still valid if used from the correct IP r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 # Another option to provide IP is through the header passed by nginx # This only works if PROXIED_CONNECTION is on # (disabled by default, for security purpose) if Env.get_bool("PROXIED_CONNECTION"): headers["X-Forwarded-For"] = faker.ipv4() # type: ignore r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401
def test_optional_access_token_parameter(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping authentication tests") return # Optional authentication can accept missing tokens r = client.get(f"{API_URI}/tests/optionalqueryauthentication") assert r.status_code == 204 headers, token = self.do_login(client, None, None) # Or valid tokens r = client.get(f"{API_URI}/tests/optionalqueryauthentication", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == BaseAuthentication.default_user # But not invalid tokens, i.e. if presented the tokens is always validated r = client.get( f"{API_URI}/tests/optionalqueryauthentication", headers={"Authorization": "Bearer invalid"}, ) assert r.status_code == 401 r = client.get( f"{API_URI}/tests/optionalqueryauthentication", query_string={"access_token": token}, ) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == BaseAuthentication.default_user r = client.get( f"{API_URI}/tests/optionalqueryauthentication", query_string={"access_token": "invalid"}, ) # invalid tokens should be rejected, but query token is ignored assert r.status_code == 401
def test_parameter_injection(self, client: FlaskClient) -> None: if Env.get_bool("AUTH_ENABLE"): headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/tests/inject/myparam", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert len(response) == 3 # User is injected by the authentication decorator assert response[0] == BaseAuthentication.default_user # myparam is injected as url parameter assert response[1] == "myparam" # default_value is injected only because it has a... default value assert response[2] == "default_value"
def test_no_auth(self, client: FlaskClient) -> None: r = client.get(f"{API_URI}/tests/noauth") assert r.status_code == 200 assert self.get_content(r) == "OK" if Env.get_bool("AUTH_ENABLE"): headers, _ = self.do_login(client, None, None) # Tokens are ignored r = client.get(f"{API_URI}/tests/noauth", headers=headers) assert r.status_code == 200 assert self.get_content(r) == "OK" # Tokens are ignored even if invalid r = client.get(f"{API_URI}/tests/noauth", headers={"Authorization": "Bearer invalid"}) assert r.status_code == 200 assert self.get_content(r) == "OK"
def test_admin_stats(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping admin/logins tests") return r = client.get(f"{API_URI}/admin/logins") assert r.status_code == 401 random_username = faker.ascii_email() self.do_login(client, random_username, faker.pystr(), status_code=401) headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/admin/logins", headers=headers) assert r.status_code == 200 logins = self.get_content(r) assert isinstance(logins, list) assert len(logins) > 0 assert "username" in logins[0] assert "date" in logins[0] assert "IP" in logins[0] assert "location" in logins[0] assert "failed" in logins[0] assert "flushed" in logins[0] for login in logins: if login["username"] == BaseAuthentication.default_user: break else: # pragma: no cover pytest.fail("Default user not found in logins table") for login in logins: if login["username"] == random_username: assert login["failed"] is True assert login["flushed"] is False break else: # pragma: no cover pytest.fail("Random user not found in logins table")
def create_group( cls, client: FlaskClient, data: Optional[Dict[str, Any]] = None) -> Tuple[str, Dict[str, Any]]: assert Env.get_bool("MAIN_LOGIN_ENABLE") admin_headers, _ = cls.do_login(client, None, None) assert admin_headers is not None schema = cls.getDynamicInputSchema(client, "admin/groups", admin_headers) group_data = cls.buildData(schema) if data: group_data.update(data) r = client.post(f"{API_URI}/admin/groups", data=group_data, headers=admin_headers) assert r.status_code == 200 uuid = cls.get_content(r) return uuid, group_data
def skip_endpoint(depends_on): for var in depends_on: pieces = var.strip().split(" ") pieces_num = len(pieces) if pieces_num == 1: dependency = pieces.pop() negate = False elif pieces_num == 2: neg, dependency = pieces negate = neg.lower() == "not" else: # pragma: no cover print_and_exit("Wrong depends_on parameter: {}", var) check = Env.get_bool(dependency) if negate: check = not check # Skip if not meeting the requirements of the dependency if not check: return True, dependency return False, None
def test_06_token_ip_validity(self, client: FlaskClient, faker: Faker) -> None: if Env.get_bool("MAIN_LOGIN_ENABLE"): if Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD") < 10: headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) assert r.status_code == 200 time.sleep(Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD")) r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) log.error( "DEBUG CODE: this 401 should be due to invalid IP address") assert r.status_code == 401 # After the failure the token is still valid if used from the corret IP r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 # Another option to provide IP is through the header passed by nginx headers["X-Forwarded-For"] = faker.ipv4() # type: ignore r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401
class TestApp(BaseTests): def test_01_login(self, client: FlaskClient, faker: Faker) -> None: """ Check that you can login and receive back your token """ log.info("*** VERIFY CASE INSENSITIVE LOGIN") # BaseAuthentication.load_default_user() # BaseAuthentication.load_roles() USER = BaseAuthentication.default_user or "just-to-prevent-None" PWD = BaseAuthentication.default_password or "just-to-prevent-None" # Login by using upper case username self.do_login(client, USER.upper(), PWD) events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == USER # Wrong credentials # Off course PWD cannot be upper :D self.do_login(client, USER, PWD.upper(), status_code=401) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == USER log.info("*** VERIFY valid credentials") # Login by using normal username (no upper case) headers, _ = self.do_login(client, None, None) events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == USER time.sleep(5) # Verify MAX_PASSWORD_VALIDITY, if set headers, token = self.do_login(client, None, None) events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == USER self.save("auth_header", headers) self.save("auth_token", token) # Verify credentials r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 c = self.get_content(r) assert isinstance(c, bool) and c # this check verifies a BUG with neo4j causing crash of auth module # when using a non-email-username to authenticate log.info("*** VERIFY with a non-email-username") self.do_login( client, "notanemail", "[A-Za-z0-9]+", status_code=400, ) # Check failure log.info("*** VERIFY invalid credentials") random_email = faker.ascii_email() self.do_login( client, random_email, faker.password(strong=True), status_code=401, ) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == random_email def test_02_GET_profile(self, client: FlaskClient, faker: Faker) -> None: """ Check if you can use your token for protected endpoints """ # Check success log.info("*** VERIFY valid token") r = client.get(f"{AUTH_URI}/profile", headers=self.get("auth_header")) assert r.status_code == 200 uuid = self.get_content(r).get("uuid") # Check failure log.info("*** VERIFY invalid token") r = client.get(f"{AUTH_URI}/profile") assert r.status_code == 401 # Token created for a fake user token = self.get_crafted_token("f") headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Token created for another user token = self.get_crafted_token("x") headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Token created for another user token = self.get_crafted_token("f", wrong_algorithm=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Token created for another user token = self.get_crafted_token("f", wrong_secret=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # token created for the correct user, but from outside the system!! token = self.get_crafted_token("f", user_id=uuid) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Immature token token = self.get_crafted_token("f", user_id=uuid, immature=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Expired token token = self.get_crafted_token("f", user_id=uuid, expired=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Sending malformed tokens headers = {"Authorization": "Bearer"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 headers = {"Authorization": f"Bearer '{faker.pystr()}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 # Bearer realm is expected to be case sensitive token = self.get("auth_token") headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 headers = {"Authorization": f"bearer {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 headers = {"Authorization": f"BEARER {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 token = self.get("auth_token") headers = {"Authorization": f"Bear {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 USER = BaseAuthentication.default_user PWD = BaseAuthentication.default_password # Testing Basic Authentication (not allowed) credentials = f"{USER}:{PWD}" encoded_credentials = base64.b64encode( str.encode(credentials)).decode("utf-8") headers = {"Authorization": f"Basic {encoded_credentials}"} r = client.post(f"{AUTH_URI}/login", headers=headers) # Response is: # { # 'password': ['Missing data for required field.'], # 'username': ['Missing data for required field.'] # } assert r.status_code == 400 r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 def test_03_change_profile(self, client: FlaskClient, faker: Faker) -> None: # Always enable during core tests if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Profile is disabled, skipping tests") return headers, _ = self.do_login(client, None, None) # update profile, no auth r = client.put(f"{AUTH_URI}/profile") assert r.status_code == 401 # update profile, no auth r = client.patch(f"{AUTH_URI}/profile") assert r.status_code == 401 # update profile, no data r = client.patch(f"{AUTH_URI}/profile", data={}, headers=headers) assert r.status_code == 204 events = self.get_last_events(1) assert events[0].event == Events.modify.value assert events[0].user == BaseAuthentication.default_user assert events[0].target_type == "User" # It is true in the core, but projects may introduce additional values # and expand the input dictionary even if initially empty # e.g. meteohub adds here the requests_expiration_days parameter # assert len(events[0].payload) == 0 newname = faker.name() newuuid = faker.pystr() r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 c = self.get_content(r) assert c.get("name") is not None assert c.get("name") != newname assert c.get("uuid") is not None assert c.get("uuid") != newuuid # update profile data = {"name": newname, "uuid": newuuid} r = client.patch(f"{AUTH_URI}/profile", data=data, headers=headers) # uuid cannot be modified and will raise an unknown field assert r.status_code == 400 data = {"name": newname} r = client.patch(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 204 events = self.get_last_events(1) assert events[0].event == Events.modify.value assert events[0].user == BaseAuthentication.default_user assert events[0].target_type == "User" # It is true in the core, but projects may introduce additional values # and expand the input dictionary even if initially empty # e.g. meteohub adds here the requests_expiration_days parameter # assert len(events[0].payload) == 1 assert "name" in events[0].payload r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 c = self.get_content(r) assert c.get("name") == newname assert c.get("uuid") != newuuid # change password, no data r = client.put(f"{AUTH_URI}/profile", data={}, headers=headers) assert r.status_code == 400 # Sending a new_password and/or password_confirm without a password newpassword = faker.password() data = {"new_password": newpassword} r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data = {"password_confirm": newpassword} r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data = {"new_password": newpassword, "password_confirm": newpassword} r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data = {} data["password"] = faker.password(length=5) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data["new_password"] = faker.password(length=5) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data["password_confirm"] = faker.password(length=5) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data["password"] = BaseAuthentication.default_password r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 # Passwords are too short data["password_confirm"] = data["new_password"] r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 # Trying to set new password == password... it is not permitted! data["password_confirm"] = data["password"] data["new_password"] = data["password"] if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp( BaseAuthentication.default_user) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 409 # Change the password data["new_password"] = faker.password(strong=True) data["password_confirm"] = data["new_password"] r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 204 # After a change password a spam of delete Token is expected # Reverse the list and skip all delete tokens to find the change password event events = self.get_last_events(100) events.reverse() for event in events: if event.event == Events.delete.value: assert event.target_type == "Token" continue assert event.event == Events.change_password.value assert event.user == BaseAuthentication.default_user break # verify the new password headers, _ = self.do_login(client, BaseAuthentication.default_user, data["new_password"]) # restore the previous password data["password"] = data["new_password"] data["new_password"] = BaseAuthentication.default_password data["password_confirm"] = BaseAuthentication.default_password if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp( BaseAuthentication.default_user) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 204 # After a change password a spam of delete Token is expected # Reverse the list and skip all delete tokens to find the change password event events = self.get_last_events(100) events.reverse() for event in events: if event.event == Events.delete.value: assert event.target_type == "Token" continue assert event.event == Events.change_password.value assert event.user == BaseAuthentication.default_user break # verify the new password headers, _ = self.do_login(client, BaseAuthentication.default_user, BaseAuthentication.default_password) self.save("auth_header", headers) def test_04_logout(self, client: FlaskClient) -> None: """ Check that you can logout with a valid token """ # Check success log.info("*** VERIFY valid token") r = client.get(f"{AUTH_URI}/logout", headers=self.get("auth_header")) assert r.status_code == 204 events = self.get_last_events(2) assert events[0].event == Events.delete.value assert events[0].user == "-" assert events[0].target_type == "Token" assert events[1].event == Events.logout.value assert events[1].user == BaseAuthentication.default_user # Check failure log.info("*** VERIFY invalid token") r = client.get(f"{AUTH_URI}/logout") assert r.status_code == 401 def test_05_login_failures(self, client: FlaskClient) -> None: if Env.get_bool("MAIN_LOGIN_ENABLE"): # Create a new user on the fly to test the cached endpoint _, data = self.create_user(client) headers, _ = self.do_login(client, data["email"], data["password"], test_failures=True) r = client.get(f"{AUTH_URI}/logout", headers=headers) assert r.status_code == 204 def test_06_token_ip_validity(self, client: FlaskClient, faker: Faker) -> None: if Env.get_bool("MAIN_LOGIN_ENABLE"): if Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD") < 10: headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) assert r.status_code == 200 time.sleep(Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD")) r = client.get( f"{AUTH_URI}/status", headers=headers, environ_base={"REMOTE_ADDR": faker.ipv4()}, ) log.error( "DEBUG CODE: this 401 should be due to invalid IP address") assert r.status_code == 401 # After the failure the token is still valid if used from the corret IP r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 # Another option to provide IP is through the header passed by nginx headers["X-Forwarded-For"] = faker.ipv4() # type: ignore r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): def test_07_totp_failures(self, client: FlaskClient, faker: Faker) -> None: uuid, data = self.create_user(client) username = data["email"] password = data["password"] new_password = faker.password(strong=True) invalid_totp = ( str(faker.pyint(min_value=0, max_value=9)), str(faker.pyint(min_value=10, max_value=99)), str(faker.pyint(min_value=100, max_value=999)), str(faker.pyint(min_value=1000, max_value=9999)), str(faker.pyint(min_value=10000, max_value=99999)), str(faker.pyint(min_value=1000000, max_value=9999999)), faker.pystr(6), ) ################################### # Test first password change ################################### data = { "username": username, "password": password, "new_password": new_password, "password_confirm": new_password, } r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 403 resp = self.get_content(r) assert "actions" in resp assert "errors" in resp assert "FIRST LOGIN" in resp["actions"] assert "TOTP" in resp["actions"] assert "Please change your temporary password" in resp["errors"] assert "You do not provided a valid verification code" in resp[ "errors"] # validate that the QR code is a valid PNG image # ... not implemented data["totp_code"] = "000000" r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 401 assert self.get_content(r) == "Verification code is not valid" events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].user == username assert "totp" in events[0].payload assert events[0].payload["totp"] == OBSCURE_VALUE for totp in invalid_totp: data["totp_code"] = totp r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 400 resp = self.get_content(r) assert "totp_code" in resp assert "Invalid TOTP format" in resp["totp_code"] data["totp_code"] = self.generate_totp(username) r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 200 events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == username password = new_password ################################### # Test login ################################### data = { "username": username, "password": password, } r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 403 resp = self.get_content(r) assert "actions" in resp assert "errors" in resp assert "TOTP" in resp["actions"] assert "You do not provided a valid verification code" in resp[ "errors"] data["totp_code"] = "000000" r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 401 assert self.get_content(r) == "Verification code is not valid" events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].user == username assert "totp" in events[0].payload assert events[0].payload["totp"] == OBSCURE_VALUE for totp in invalid_totp: data["totp_code"] = totp r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 400 resp = self.get_content(r) assert "totp_code" in resp assert "Invalid TOTP format" in resp["totp_code"] data["totp_code"] = self.generate_totp(username) r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 200 events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == username ################################### # Test password change ################################### new_password = faker.password(strong=True) headers, _ = self.do_login(client, username, password) data = { "password": password, "new_password": new_password, "password_confirm": new_password, } r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 401 assert self.get_content(r) == "Verification code is missing" data["totp_code"] = "000000" r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 401 assert self.get_content(r) == "Verification code is not valid" events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].user == username assert "totp" in events[0].payload assert events[0].payload["totp"] == OBSCURE_VALUE for totp in invalid_totp: data["totp_code"] = totp r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 resp = self.get_content(r) assert "totp_code" in resp assert "Invalid TOTP format" in resp["totp_code"] data["totp_code"] = self.generate_totp(username) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 204 # After a change password a spam of delete Token is expected # Reverse the list and skip all delete tokens to find the change pwd event events = self.get_last_events(100) events.reverse() for event in events: if event.event == Events.delete.value: assert event.target_type == "Token" continue assert event.event == Events.change_password.value assert event.user == username break # verify the new password headers, _ = self.do_login(client, username, new_password) assert headers is not None ################################### # Goodbye temporary user ################################### self.delete_user(client, uuid)
def test_03_change_profile(self, client: FlaskClient, faker: Faker) -> None: # Always enable during core tests if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Profile is disabled, skipping tests") return headers, _ = self.do_login(client, None, None) # update profile, no auth r = client.put(f"{AUTH_URI}/profile") assert r.status_code == 401 # update profile, no auth r = client.patch(f"{AUTH_URI}/profile") assert r.status_code == 401 # update profile, no data r = client.patch(f"{AUTH_URI}/profile", data={}, headers=headers) assert r.status_code == 204 events = self.get_last_events(1) assert events[0].event == Events.modify.value assert events[0].user == BaseAuthentication.default_user assert events[0].target_type == "User" # It is true in the core, but projects may introduce additional values # and expand the input dictionary even if initially empty # e.g. meteohub adds here the requests_expiration_days parameter # assert len(events[0].payload) == 0 newname = faker.name() newuuid = faker.pystr() r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 c = self.get_content(r) assert c.get("name") is not None assert c.get("name") != newname assert c.get("uuid") is not None assert c.get("uuid") != newuuid # update profile data = {"name": newname, "uuid": newuuid} r = client.patch(f"{AUTH_URI}/profile", data=data, headers=headers) # uuid cannot be modified and will raise an unknown field assert r.status_code == 400 data = {"name": newname} r = client.patch(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 204 events = self.get_last_events(1) assert events[0].event == Events.modify.value assert events[0].user == BaseAuthentication.default_user assert events[0].target_type == "User" # It is true in the core, but projects may introduce additional values # and expand the input dictionary even if initially empty # e.g. meteohub adds here the requests_expiration_days parameter # assert len(events[0].payload) == 1 assert "name" in events[0].payload r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 c = self.get_content(r) assert c.get("name") == newname assert c.get("uuid") != newuuid # change password, no data r = client.put(f"{AUTH_URI}/profile", data={}, headers=headers) assert r.status_code == 400 # Sending a new_password and/or password_confirm without a password newpassword = faker.password() data = {"new_password": newpassword} r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data = {"password_confirm": newpassword} r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data = {"new_password": newpassword, "password_confirm": newpassword} r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data = {} data["password"] = faker.password(length=5) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data["new_password"] = faker.password(length=5) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data["password_confirm"] = faker.password(length=5) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data["password"] = BaseAuthentication.default_password r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 # Passwords are too short data["password_confirm"] = data["new_password"] r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 # Trying to set new password == password... it is not permitted! data["password_confirm"] = data["password"] data["new_password"] = data["password"] if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp( BaseAuthentication.default_user) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 409 # Change the password data["new_password"] = faker.password(strong=True) data["password_confirm"] = data["new_password"] r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 204 # After a change password a spam of delete Token is expected # Reverse the list and skip all delete tokens to find the change password event events = self.get_last_events(100) events.reverse() for event in events: if event.event == Events.delete.value: assert event.target_type == "Token" continue assert event.event == Events.change_password.value assert event.user == BaseAuthentication.default_user break # verify the new password headers, _ = self.do_login(client, BaseAuthentication.default_user, data["new_password"]) # restore the previous password data["password"] = data["new_password"] data["new_password"] = BaseAuthentication.default_password data["password_confirm"] = BaseAuthentication.default_password if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp( BaseAuthentication.default_user) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 204 # After a change password a spam of delete Token is expected # Reverse the list and skip all delete tokens to find the change password event events = self.get_last_events(100) events.reverse() for event in events: if event.event == Events.delete.value: assert event.target_type == "Token" continue assert event.event == Events.change_password.value assert event.user == BaseAuthentication.default_user break # verify the new password headers, _ = self.do_login(client, BaseAuthentication.default_user, BaseAuthentication.default_password) self.save("auth_header", headers)
def test_01_login(self, client: FlaskClient, faker: Faker) -> None: """Check that you can login and receive back your token""" if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping login tests") return log.info("*** VERIFY CASE INSENSITIVE LOGIN") # BaseAuthentication.load_default_user() # BaseAuthentication.load_roles() USER = BaseAuthentication.default_user or "just-to-prevent-None" PWD = BaseAuthentication.default_password or "just-to-prevent-None" # Login by using upper case username self.do_login(client, USER.upper(), PWD) events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == USER assert events[0].url == "/auth/login" auth = Connector.get_authentication_instance() logins = auth.get_logins(USER) login = logins[-1] assert login.username == USER # Wrong credentials # Off course PWD cannot be upper :D self.do_login(client, USER, PWD.upper(), status_code=401) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == USER assert events[0].url == "/auth/login" logins = auth.get_logins(USER) login = logins[-1] assert login.username == USER log.info("*** VERIFY valid credentials") # Login by using normal username (no upper case) headers, _ = self.do_login(client, None, None) events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == USER assert events[0].url == "/auth/login" time.sleep(5) # Verify MAX_PASSWORD_VALIDITY, if set headers, token = self.do_login(client, None, None) events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == USER assert events[0].url == "/auth/login" self.save("auth_header", headers) self.save("auth_token", token) # Verify credentials r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 c = self.get_content(r) assert isinstance(c, bool) and c # this check verifies a BUG with neo4j causing crash of auth module # when using a non-email-username to authenticate log.info("*** VERIFY with a non-email-username") self.do_login( client, "notanemail", "[A-Za-z0-9]+", status_code=400, ) # Check failure log.info("*** VERIFY invalid credentials") random_email = faker.ascii_email() self.do_login( client, random_email, faker.password(strong=True), status_code=401, ) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == random_email assert events[0].url == "/auth/login"