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
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
""" This will used credentials created at the beginning of the suite to verify that unused credentialas are banned """ from faker import Faker from restapi.env import Env from restapi.tests import AUTH_URI, BaseTests, FlaskClient from restapi.utilities.logs import Events if Env.get_int("AUTH_DISABLE_UNUSED_CREDENTIALS_AFTER") > 0: class TestApp1(BaseTests): def test_test_unused_credentials(self, client: FlaskClient, faker: Faker) -> None: assert BaseTests.unused_credentials is not None assert len(BaseTests.unused_credentials) == 3 data = { "username": BaseTests.unused_credentials[0], "password": faker.password(strong=True), } # Credentials are verified before the inactivity check r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 401 resp = self.get_content(r) assert resp == "Invalid access credentials" data = {
MODELS_DIR = "models" CONF_PATH = Path("confs") # Also configured in controller EXTENDED_PROJECT_DISABLED = "no_extended_project" BACKEND_PACKAGE = "restapi" # package inside rapydo-http CUSTOM_PACKAGE = Env.get("PROJECT_NAME", "custom") EXTENDED_PACKAGE = Env.get("EXTENDED_PACKAGE", EXTENDED_PROJECT_DISABLED) SENTRY_URL = Env.get("SENTRY_URL", "").strip() or None ABS_RESTAPI_PATH = Path(__file__).resolve().parent GZIP_ENABLE = Env.get_bool("GZIP_COMPRESSION_ENABLE") GZIP_THRESHOLD = max(0, Env.get_int("GZIP_COMPRESSION_THRESHOLD")) GZIP_LEVEL = max(1, min(9, Env.get_int("GZIP_COMPRESSION_LEVEL"))) @lru_cache def get_project_configuration(key: str, default: str) -> str: return glom(mem.configuration, key, default=default) @lru_cache def get_backend_url() -> str: BACKEND_URL = Env.get("BACKEND_URL", "") if BACKEND_URL: return BACKEND_URL
from restapi.connectors import celery, neo4j from restapi.env import Env from restapi.utilities.logs import log log.info("Starting init pipeline cron") # get the list of datasets ready to be analysed graph = neo4j.get_instance() datasets_to_analise = graph.Dataset.nodes.filter( status="UPLOAD COMPLETED").all() # get all the dataset uuid datasets_uuid = [x.uuid for x in datasets_to_analise] chunks_limit = Env.get_int("CHUNKS_LIMIT", 16) for chunk in [ datasets_uuid[i:i + chunks_limit] for i in range(0, len(datasets_uuid), chunks_limit) ]: log.info("Sending pipeline for datasets: {}", chunk) # pass the chunk to the celery task c = celery.get_instance() task = c.celery_app.send_task( "launch_pipeline", args=(chunk, ), countdown=1, ) log.info("{} datasets sent to task {}", len(chunk), task) # mark the related datasets as "QUEUED"
def test_registration(self, client: FlaskClient, faker: Faker) -> None: # Always enabled during core tests if not Env.get_bool("ALLOW_REGISTRATION"): # pragma: no cover log.warning("User registration is disabled, skipping tests") return project_tile = get_project_configuration("project.title", default="YourProject") proto = "https" if PRODUCTION else "http" # registration, empty input r = client.post(f"{AUTH_URI}/profile") assert r.status_code == 400 # registration, missing information r = client.post(f"{AUTH_URI}/profile", data={"x": "y"}) assert r.status_code == 400 registration_data = {} registration_data["password"] = faker.password(5) r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["email"] = BaseAuthentication.default_user r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["name"] = faker.first_name() r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["surname"] = faker.last_name() r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["password_confirm"] = faker.password(strong=True) r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999) registration_data["password"] = faker.password(min_pwd_len - 1) r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["password"] = faker.password(min_pwd_len) r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = f"This user already exists: {BaseAuthentication.default_user}" assert self.get_content(r) == m registration_data["email"] = faker.ascii_email() r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 assert self.get_content( r) == "Your password doesn't match the confirmation" registration_data["password"] = faker.password(min_pwd_len, low=False, up=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = "Password is too weak, missing lower case letters" assert self.get_content(r) == m registration_data["password"] = faker.password(min_pwd_len, low=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = "Password is too weak, missing upper case letters" assert self.get_content(r) == m registration_data["password"] = faker.password(min_pwd_len, low=True, up=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = "Password is too weak, missing numbers" assert self.get_content(r) == m registration_data["password"] = faker.password(min_pwd_len, low=True, up=True, digits=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = "Password is too weak, missing special characters" assert self.get_content(r) == m registration_data["password"] = faker.password(strong=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) # now the user is created but INACTIVE, activation endpoint is needed assert r.status_code == 200 registration_message = "We are sending an email to your email address where " registration_message += "you will find the link to activate your account" assert self.get_content(r) == registration_message events = self.get_last_events(1) assert events[0].event == Events.create.value assert events[0].user == "-" assert events[0].target_type == "User" assert "name" in events[0].payload assert "password" in events[0].payload assert events[0].payload["password"] == OBSCURE_VALUE mail = self.read_mock_email() body = mail.get("body") assert body is not None assert mail.get("headers") is not None # Subject: is a key in the MIMEText assert f"Subject: {project_tile} account activation" in mail.get( "headers") assert f"{proto}://localhost/public/register/" in body # This will fail because the user is not active _, error = self.do_login( client, registration_data["email"], registration_data["password"], status_code=403, ) assert error == "Sorry, this account is not active" # Also password reset is not allowed data = {"reset_email": registration_data["email"]} r = client.post(f"{AUTH_URI}/reset", data=data) assert r.status_code == 403 assert self.get_content(r) == "Sorry, this account is not active" events = self.get_last_events(2) assert events[0].event == Events.refused_login.value assert events[0].payload["username"] == data["reset_email"] assert events[0].payload["motivation"] == "account not active" assert events[1].event == Events.refused_login.value assert events[1].payload["username"] == data["reset_email"] assert events[1].payload["motivation"] == "account not active" # Activation, missing or wrong information r = client.post(f"{AUTH_URI}/profile/activate") assert r.status_code == 400 r = client.post(f"{AUTH_URI}/profile/activate", data=faker.pydict(2)) assert r.status_code == 400 # It isn't an email invalid = faker.pystr(10) r = client.post(f"{AUTH_URI}/profile/activate", data={"username": invalid}) assert r.status_code == 400 headers, _ = self.do_login(client, None, None) activation_message = "We are sending an email to your email address where " activation_message += "you will find the link to activate your account" # request activation, wrong username r = client.post(f"{AUTH_URI}/profile/activate", data={"username": faker.ascii_email()}) # return is 200, but no token will be generated and no mail will be sent # but it respond with the activation msg and hides the non existence of the user assert r.status_code == 200 assert self.get_content(r) == activation_message events = self.get_last_events(1) assert events[0].event != Events.activation.value assert self.read_mock_email() is None # request activation, correct username r = client.post( f"{AUTH_URI}/profile/activate", data={"username": registration_data["email"]}, ) assert r.status_code == 200 assert self.get_content(r) == activation_message mail = self.read_mock_email() body = mail.get("body") assert body is not None assert mail.get("headers") is not None # Subject: is a key in the MIMEText assert f"Subject: {project_tile} account activation" in mail.get( "headers") assert f"{proto}://localhost/public/register/" in body token = self.get_token_from_body(body) assert token is not None # profile activation r = client.put(f"{AUTH_URI}/profile/activate/thisisatoken") # this token is not valid assert r.status_code == 400 # profile activation r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 200 assert self.get_content(r) == "Account activated" events = self.get_last_events(1) assert events[0].event == Events.activation.value assert events[0].user == registration_data["email"] assert events[0].target_type == "User" # Activation token is no longer valid r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 assert self.get_content(r) == "Invalid activation token" # Token created for another user token = self.get_crafted_token("a") r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # Token created for another user token = self.get_crafted_token("a", wrong_algorithm=True) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # Token created for another user token = self.get_crafted_token("a", wrong_secret=True) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 uuid = self.get_content(r).get("uuid") token = self.get_crafted_token("x", user_id=uuid) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # token created for the correct user, but from outside the system!! token = self.get_crafted_token("a", user_id=uuid) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # Immature token token = self.get_crafted_token("a", user_id=uuid, immature=True) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # Expired token token = self.get_crafted_token("a", user_id=uuid, expired=True) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token: this request is expired" # Testing the following use case: # 1 - user registration # 2 - user activation using unconventional channel, e.g. by admins # 3 - user tries to activate and fails because already active registration_data["email"] = faker.ascii_email() r = client.post(f"{AUTH_URI}/profile", data=registration_data) # now the user is created but INACTIVE, activation endpoint is needed assert r.status_code == 200 mail = self.read_mock_email() body = mail.get("body") assert body is not None assert mail.get("headers") is not None assert f"{proto}://localhost/public/register/" in body token = self.get_token_from_body(body) assert token is not None headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/admin/users", headers=headers) assert r.status_code == 200 users = self.get_content(r) uuid = None for u in users: if u.get("email") == registration_data["email"]: uuid = u.get("uuid") break assert uuid is not None r = client.put(f"{API_URI}/admin/users/{uuid}", data={"is_active": True}, headers=headers) assert r.status_code == 204 r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token: this request is no longer valid" r = client.get(f"{API_URI}/admin/tokens", headers=headers) content = self.get_content(r) for t in content: if t.get("token") == token: # pragma: no cover pytest.fail( "Token not properly invalidated, still bount to user {}", t.get(id))
class BaseAuthentication(metaclass=abc.ABCMeta): """ An almost abstract class with methods to be implemented with a new service that aims to store credentials of users and roles. """ # Secret loaded from secret.key file JWT_SECRET: Optional[bytes] = None # JWT_ALGO = 'HS256' # Should be faster on 64bit machines JWT_ALGO = "HS512" # 1 month in seconds DEFAULT_TOKEN_TTL = Env.get_int("AUTH_JWT_TOKEN_TTL", 2_592_000) # Grace period before starting to evaluate IP address on token validation GRACE_PERIOD = timedelta(seconds=Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD", 7200)) SAVE_LAST_ACCESS_EVERY = timedelta( seconds=Env.get_int("AUTH_TOKEN_SAVE_FREQUENCY", 60) ) FULL_TOKEN = "f" PWD_RESET = "r" ACTIVATE_ACCOUNT = "a" TOTP = "TOTP" MIN_PASSWORD_LENGTH = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 8) SECOND_FACTOR_AUTHENTICATION = Env.get_bool( "AUTH_SECOND_FACTOR_AUTHENTICATION", False ) # enabled if explicitly set or for 2FA is enabled FORCE_FIRST_PASSWORD_CHANGE = SECOND_FACTOR_AUTHENTICATION or Env.get_bool( "AUTH_FORCE_FIRST_PASSWORD_CHANGE", False ) # enabled if explicitly set or for 2FA is enabled VERIFY_PASSWORD_STRENGTH = SECOND_FACTOR_AUTHENTICATION or Env.get_bool( "AUTH_VERIFY_PASSWORD_STRENGTH", False ) MAX_PASSWORD_VALIDITY: Optional[timedelta] = get_timedelta( Env.get_int("AUTH_MAX_PASSWORD_VALIDITY", 0), MAX_PASSWORD_VALIDITY_MIN_TESTNIG_VALUE, ) DISABLE_UNUSED_CREDENTIALS_AFTER: Optional[timedelta] = get_timedelta( Env.get_int("AUTH_DISABLE_UNUSED_CREDENTIALS_AFTER", 0), # min 60 seconds are required when testing DISABLE_UNUSED_CREDENTIALS_AFTER_MIN_TESTNIG_VALUE, ) MAX_LOGIN_ATTEMPTS = get_max_login_attempts( Env.get_int("AUTH_MAX_LOGIN_ATTEMPTS", 0) ) FAILED_LOGINS_EXPIRATION: timedelta = timedelta( seconds=Env.get_int("AUTH_LOGIN_BAN_TIME", 3600) ) default_user: Optional[str] = None default_password: Optional[str] = None roles: List[str] = [] roles_data: Dict[str, str] = {} default_role: str = Role.USER.value # To be stored on DB failed_logins: Dict[str, List[FailedLogin]] = {} # Executed once by Connector in init_app @classmethod def module_initialization(cls) -> None: cls.load_default_user() cls.load_roles() cls.import_secret(SECRET_KEY_FILE) @staticmethod def load_default_user() -> None: BaseAuthentication.default_user = Env.get("AUTH_DEFAULT_USERNAME") BaseAuthentication.default_password = Env.get("AUTH_DEFAULT_PASSWORD") if ( BaseAuthentication.default_user is None or BaseAuthentication.default_password is None ): # pragma: no cover print_and_exit("Default credentials are unavailable!") @staticmethod def load_roles() -> None: BaseAuthentication.roles_data = get_project_configuration( "variables.roles" ).copy() if not BaseAuthentication.roles_data: # pragma: no cover print_and_exit("No roles configured") BaseAuthentication.default_role = BaseAuthentication.roles_data.pop("default") BaseAuthentication.roles = [] for role, description in BaseAuthentication.roles_data.items(): if description != ROLE_DISABLED: BaseAuthentication.roles.append(role) if not BaseAuthentication.default_role: # pragma: no cover print_and_exit( "Default role {} not available!", BaseAuthentication.default_role ) def make_login(self, username: str, password: str) -> Tuple[str, Payload, User]: """ The method which will check if credentials are good to go """ try: user = self.get_user(username=username) except ValueError as e: # pragma: no cover # SqlAlchemy can raise the following error: # A string literal cannot contain NUL (0x00) characters. log.error(e) raise BadRequest("Invalid input received") except BaseException as e: # pragma: no cover log.error("Unable to connect to auth backend\n[{}] {}", type(e), e) raise ServiceUnavailable("Unable to connect to auth backend") if user is None: self.register_failed_login(username) self.log_event( Events.failed_login, payload={"username": username}, user=user, ) raise Unauthorized("Invalid access credentials", is_warning=True) # Check if Oauth2 is enabled if user.authmethod != "credentials": # pragma: no cover raise BadRequest("Invalid authentication method") # New hashing algorithm, based on bcrypt if self.verify_password(password, user.password): # Token expiration is capped by the user expiration date, if set payload, full_payload = self.fill_payload(user, expiration=user.expiration) token = self.create_token(payload) self.log_event(Events.login, user=user) return token, full_payload, user self.log_event( Events.failed_login, payload={"username": username}, user=user, ) self.register_failed_login(username) raise Unauthorized("Invalid access credentials", is_warning=True) @classmethod def import_secret(cls, abs_filename: str) -> None: try: cls.JWT_SECRET = open(abs_filename, "rb").read() except OSError: # pragma: no cover print_and_exit("Jwt secret file {} not found", abs_filename) # ##################### # # Password handling # #################### @staticmethod def verify_password(plain_password: str, hashed_password: str) -> bool: try: return cast(bool, pwd_context.verify(plain_password, hashed_password)) except ValueError as e: # pragma: no cover log.error(e) return False @staticmethod def get_password_hash(password): if not password: raise Unauthorized("Invalid password") return pwd_context.hash(password) @staticmethod def get_remote_ip() -> str: try: if forwarded_ips := request.headers.getlist("X-Forwarded-For"): # it can be something like: ['IP1, IP2'] return str(forwarded_ips[-1].split(",")[0].strip()) if PRODUCTION and not TESTING: # pragma: no cover log.warning( "Production mode is enabled, but X-Forwarded-For header is missing" ) if request.remote_addr: return request.remote_addr # Raised when get_remote_ip is executed outside request context # For example when creating tokens in initialize_testing_environment except RuntimeError as e: log.debug(e)
def connect(self, **kwargs: str) -> "FTPExt": variables = self.variables.copy() variables.update(kwargs) if (host := variables.get("host")) is None: # pragma: no cover raise ServiceUnavailable("Missing hostname") if (user := variables.get("user")) is None: # pragma: no cover raise ServiceUnavailable("Missing credentials") if (password := variables.get("password")) is None: # pragma: no cover raise ServiceUnavailable("Missing credentials") port = Env.get_int(variables.get("port"), 21) ssl_enabled = Env.to_bool(variables.get("ssl_enabled")) if ssl_enabled: context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) context.load_default_certs() # Disable certificate verification: # context.verify_mode = ssl.CERT_NONE # Enable certificate verification: context.verify_mode = ssl.CERT_REQUIRED context.check_hostname = False # Path to pem file to verify self signed certificates context.load_verify_locations(cafile=SSL_CERTIFICATE)
def test_password_management(self, faker: Faker) -> None: # Ensure name and surname longer than 3 name = self.get_first_name(faker) surname = self.get_last_name(faker) # Ensure an email not containing name and surname email = self.get_random_email(faker, name, surname) auth = Connector.get_authentication_instance() min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999) pwd = faker.password(min_pwd_len - 1) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "The new password cannot match the previous password" pwd = faker.password(min_pwd_len - 1) old_pwd = faker.password(min_pwd_len) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val error = f"Password is too short, use at least {min_pwd_len} characters" assert ret_text == error pwd = faker.password(min_pwd_len, low=False, up=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "Password is too weak, missing lower case letters" pwd = faker.password(min_pwd_len, low=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "Password is too weak, missing upper case letters" pwd = faker.password(min_pwd_len, low=True, up=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "Password is too weak, missing numbers" pwd = faker.password(min_pwd_len, low=True, up=True, digits=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "Password is too weak, missing special characters" pwd = faker.password(min_pwd_len, low=True, up=True, digits=True, symbols=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert ret_val assert ret_text == "" password_with_name = [ name, surname, f"{faker.pystr()}{name}{faker.pystr()}" f"{faker.pystr()}{surname}{faker.pystr()}" f"{name}{faker.pyint(1, 99)}", ] for p in password_with_name: for pp in [p, p.lower(), p.upper(), p.title()]: # This is because with "strange characters" it is not ensured that: # str == str.upper().lower() # In that case let's skip the variant that alter the characters if p.lower() != pp.lower(): # pragma: no cover continue # This is to prevent failures for other reasons like length of chars pp += "+ABCabc123!" val, text = auth.verify_password_strength(pwd=pp, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not val assert text == "Password is too weak, can't contain your name" email_local = email.split("@")[0] password_with_email = [ email, email.replace(".", "").replace("_", ""), email_local, email_local.replace(".", "").replace("_", ""), f"{faker.pystr()}{email_local}{faker.pystr()}", ] for p in password_with_email: for pp in [p, p.lower(), p.upper(), p.title()]: # This is because with "strange characters" it is not ensured that: # str == str.upper().lower() # In that case let's skip the variant that alter the characters if p.lower() != pp.lower(): # pragma: no cover continue # This is to prevent failures for other reasons like length of chars pp += "+ABCabc123!" val, txt = auth.verify_password_strength(pwd=pp, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not val assert txt == "Password is too weak, can't contain your email address" # Short names are not inspected for containing checks ret_val, ret_text = auth.verify_password_strength(pwd="Bob1234567!", old_pwd=old_pwd, email=email, name="Bob", surname=surname) assert ret_val assert ret_text == "" user = auth.get_user(username=BaseAuthentication.default_user) pwd = faker.password(min_pwd_len - 1) with pytest.raises(BadRequest, match=r"Missing new password"): # None password auth.change_password(user, pwd, None, None) with pytest.raises(BadRequest, match=r"Missing password confirmation"): # None password confirmation auth.change_password(user, pwd, pwd, None) with pytest.raises( Conflict, match=r"Your password doesn't match the confirmation"): # wrong confirmation auth.change_password(user, pwd, pwd, faker.password(strong=True)) with pytest.raises( Conflict, match=r"The new password cannot match the previous password", ): # Failed password strength checks auth.change_password(user, pwd, pwd, pwd) with pytest.raises( Conflict, match= rf"Password is too short, use at least {min_pwd_len} characters", ): # the first password parameter is only checked for new password strenght # i.e. is verified password != newpassword # pwd validity will be checked once completed checks on new password # => a random current password is ok here # Failed password strength checks auth.change_password(user, faker.password(), pwd, pwd) pwd1 = faker.password(strong=True) pwd2 = faker.password(strong=True) hash_1 = auth.get_password_hash(pwd1) assert len(hash_1) > 0 assert hash_1 != auth.get_password_hash(pwd2) with pytest.raises(Unauthorized, match=r"Invalid password"): # Hashing empty password auth.get_password_hash("") with pytest.raises(Unauthorized, match=r"Invalid password"): # Hashing a None password! auth.get_password_hash(None) assert auth.verify_password(pwd1, hash_1) with pytest.raises(TypeError): # Hashing a None password auth.verify_password(None, hash_1) # type: ignore assert not auth.verify_password(pwd1, None) # type: ignore assert not auth.verify_password(None, None) # type: ignore
diagnose=False, filter=print_message_on_stderr, ) setattr(set_logger, "log_id", log_id) set_logger(log_level) # Logs utilities # Can't understand why mypy is unable to understand Env.get_int, since it is annotated # with `-> int` .. but mypy raises: # Cannot determine type of 'get_int' # mypy: ignore-errors MAX_CHAR_LEN = Env.get_int("MAX_LOGS_LENGTH", 200) OBSCURE_VALUE = "****" OBSCURED_FIELDS = [ "password", "pwd", "token", "access_token", "file", "filename", "new_password", "password_confirm", "totp", "totp_code", ]
class BaseAuthentication(metaclass=ABCMeta): """ An almost abstract class with methods to be implemented with a new service that aims to store credentials of users and roles. """ JWT_SECRET: str = import_secret(JWT_SECRET_FILE).decode() fernet = Fernet(import_secret(TOTP_SECRET_FILE)) # JWT_ALGO = 'HS256' # Should be faster on 64bit machines JWT_ALGO = "HS512" # 1 month in seconds DEFAULT_TOKEN_TTL = Env.get_int("AUTH_JWT_TOKEN_TTL", 2_592_000) # Grace period before starting to evaluate IP address on token validation GRACE_PERIOD = timedelta( seconds=Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD", 7200)) SAVE_LAST_ACCESS_EVERY = timedelta( seconds=Env.get_int("AUTH_TOKEN_SAVE_FREQUENCY", 60)) FULL_TOKEN = "f" PWD_RESET = "r" ACTIVATE_ACCOUNT = "a" UNLOCK_CREDENTIALS = "u" TOTP = "TOTP" MIN_PASSWORD_LENGTH = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 8) SECOND_FACTOR_AUTHENTICATION = Env.get_bool( "AUTH_SECOND_FACTOR_AUTHENTICATION", False) TOTP_VALIDITY_WINDOW = Env.get_int("AUTH_TOTP_VALIDITY_WINDOW", 1) # enabled if explicitly set or for 2FA is enabled FORCE_FIRST_PASSWORD_CHANGE = SECOND_FACTOR_AUTHENTICATION or Env.get_bool( "AUTH_FORCE_FIRST_PASSWORD_CHANGE", False) MAX_PASSWORD_VALIDITY: Optional[timedelta] = get_timedelta( Env.get_int("AUTH_MAX_PASSWORD_VALIDITY", 0), MAX_PASSWORD_VALIDITY_MIN_TESTNIG_VALUE, ) DISABLE_UNUSED_CREDENTIALS_AFTER: Optional[timedelta] = get_timedelta( Env.get_int("AUTH_DISABLE_UNUSED_CREDENTIALS_AFTER", 0), # min 60 seconds are required when testing DISABLE_UNUSED_CREDENTIALS_AFTER_MIN_TESTNIG_VALUE, ) MAX_LOGIN_ATTEMPTS = get_max_login_attempts( Env.get_int("AUTH_MAX_LOGIN_ATTEMPTS", 8)) FAILED_LOGINS_EXPIRATION: timedelta = timedelta( seconds=get_login_ban_time(Env.get_int("AUTH_LOGIN_BAN_TIME", 3600))) default_user: Optional[str] = None default_password: Optional[str] = None roles: List[str] = [] roles_data: Dict[str, str] = {} default_role: str = Role.USER.value role_descriptions: Dict[str, str] = {} # This is to let inform mypy about the existence of self.db def __init__(self) -> None: # pragma: no cover self.db: "Connector" # Executed once by Connector in init_app @classmethod def module_initialization(cls) -> None: cls.load_default_user() cls.load_roles() @staticmethod def load_default_user() -> None: BaseAuthentication.default_user = Env.get("AUTH_DEFAULT_USERNAME", "") BaseAuthentication.default_password = Env.get("AUTH_DEFAULT_PASSWORD", "") if (not BaseAuthentication.default_user or not BaseAuthentication.default_password): # pragma: no cover print_and_exit("Default credentials are unavailable!") @staticmethod def load_roles() -> None: empty_dict: Dict[str, str] = {} BaseAuthentication.roles_data = glom(mem.configuration, "variables.roles", default=empty_dict).copy() if not BaseAuthentication.roles_data: # pragma: no cover print_and_exit("No roles configured") BaseAuthentication.default_role = BaseAuthentication.roles_data.pop( "default", "") BaseAuthentication.role_descriptions = glom( mem.configuration, "variables.roles_descriptions", default=empty_dict).copy() if not BaseAuthentication.default_role: # pragma: no cover print_and_exit("Default role not available!") BaseAuthentication.roles = [] for role, description in BaseAuthentication.roles_data.items(): if description != ROLE_DISABLED: BaseAuthentication.roles.append(role) def make_login(self, username: str, password: str, totp_code: Optional[str]) -> Tuple[str, Payload, User]: self.verify_blocked_username(username) try: user = self.get_user(username=username) except ValueError as e: # pragma: no cover # SqlAlchemy can raise the following error: # A string literal cannot contain NUL (0x00) characters. log.error(e) raise BadRequest("Invalid input received") except Exception as e: # pragma: no cover log.error("Unable to connect to auth backend\n[{}] {}", type(e), e) raise ServiceUnavailable("Unable to connect to auth backend") if user is None: self.register_failed_login(username, user=None) self.log_event( Events.failed_login, payload={"username": username}, user=user, ) raise Unauthorized("Invalid access credentials", is_warning=True) # Currently only credentials are allowed if user.authmethod != "credentials": # pragma: no cover raise BadRequest("Invalid authentication method") if not self.verify_password(password, user.password): self.log_event( Events.failed_login, payload={"username": username}, user=user, ) self.register_failed_login(username, user=user) raise Unauthorized("Invalid access credentials", is_warning=True) self.verify_user_status(user) if self.SECOND_FACTOR_AUTHENTICATION and not totp_code: raise AuthMissingTOTP() if totp_code: self.verify_totp(user, totp_code) # Token expiration is capped by the user expiration date, if set payload, full_payload = self.fill_payload(user, expiration=user.expiration) token = self.create_token(payload) self.save_login(username, user, failed=False) self.log_event(Events.login, user=user) return token, full_payload, user # ##################### # # Password handling # #################### @staticmethod def verify_password(plain_password: str, hashed_password: str) -> bool: try: return cast(bool, pwd_context.verify(plain_password, hashed_password)) except ValueError as e: # pragma: no cover log.error(e) return False @staticmethod def get_password_hash(password: Optional[str]) -> str: if not password: raise Unauthorized("Invalid password") # CryptContext is no typed.. but this is a string! return cast(str, pwd_context.hash(password)) @staticmethod def get_remote_ip(raise_warnings: bool = True) -> str: try: # Syntax: X-Forwarded-For: <client>, <proxy1>, <proxy2> # <client> The client IP address # <proxy1>, <proxy2> If a request goes through multiple proxies, the # IP addresses of each successive proxy is listed. This means, the # right-most IP address is the IP address of the most recent proxy # and the left-most IP address is the IP address of the originating # client. if PROXIED_CONNECTION: header_key = "X-Forwarded-For" if forwarded_ips := request.headers.getlist(header_key): # it can be something like: ['IP1, IP2'] return str(forwarded_ips[0].split(",")[0].strip()) # Standard (and more secure) way to obtain remote IP else:
def test_admin(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping admin authorizations tests") return # List of all paths to be tested. After each test a path will be removed. # At the end the list is expected to be empty paths = self.get_paths(client) uuid, data = self.create_user(client, roles=[Role.ADMIN]) headers, _ = self.do_login(client, data.get("email"), data.get("password")) # These are public paths = self.check_endpoint(client, "GET", "/api/status", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/specs", headers, True, paths) paths = self.check_endpoint(client, "POST", "/auth/login", headers, True, paths) if Env.get_int("AUTH_MAX_LOGIN_ATTEMPTS") > 0: paths = self.check_endpoint(client, "POST", "/auth/login/unlock/<token>", headers, True, paths) if Env.get_bool("ALLOW_REGISTRATION"): paths = self.check_endpoint(client, "POST", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "POST", "/auth/profile/activate", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/auth/profile/activate/<token>", headers, True, paths) if Env.get_bool("ALLOW_PASSWORD_RESET" ) and Connector.check_availability("smtp"): paths = self.check_endpoint(client, "POST", "/auth/reset", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/auth/reset/<token>", headers, True, paths) # These are allowed to each user paths = self.check_endpoint(client, "GET", "/auth/status", headers, True, paths) paths = self.check_endpoint(client, "GET", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "PATCH", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "GET", "/auth/tokens", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/auth/tokens/<token>", headers, True, paths) # These are allowed to coordinators paths = self.check_endpoint(client, "GET", "/api/group/users", headers, False, paths) # These are allowed to staff # ... none # These are allowed to admins paths = self.check_endpoint(client, "GET", "/api/admin/users", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/users/<user_id>", headers, True, paths) paths = self.check_endpoint(client, "POST", "/api/admin/users", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/api/admin/users/<user_id>", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/api/admin/users/<user_id>", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/groups", headers, True, paths) paths = self.check_endpoint(client, "POST", "/api/admin/groups", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/api/admin/groups/<group_id>", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/api/admin/groups/<group_id>", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/logins", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/tokens", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/api/admin/tokens/<token>", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/stats", headers, True, paths) paths = self.check_endpoint(client, "POST", "/api/admin/mail", headers, True, paths) # logout MUST be the last one or the token will be invalidated!! :-) paths = self.check_endpoint(client, "GET", "/auth/logout", headers, True, paths) assert paths == [] self.delete_user(client, uuid)
def test_password_reset(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("ALLOW_PASSWORD_RESET") or not Env.get_bool("AUTH_ENABLE"): log.warning("Password reset is disabled, skipping tests") return project_tile = get_project_configuration("project.title", default="YourProject") proto = "https" if PRODUCTION else "http" # Request password reset, missing information r = client.post(f"{AUTH_URI}/reset") assert r.status_code == 400 # Request password reset, missing information r = client.post(f"{AUTH_URI}/reset", json=faker.pydict(2)) assert r.status_code == 400 headers, _ = self.do_login(client, None, None) # Request password reset, wrong email wrong_email = faker.ascii_email() data = {"reset_email": wrong_email} r = client.post(f"{AUTH_URI}/reset", json=data) assert r.status_code == 403 msg = f"Sorry, {wrong_email} is not recognized as a valid username" assert self.get_content(r) == msg # Request password reset, correct email data = {"reset_email": BaseAuthentication.default_user} r = client.post(f"{AUTH_URI}/reset", json=data) assert r.status_code == 200 events = self.get_last_events(1) assert events[0].event == Events.reset_password_request.value assert events[0].user == data["reset_email"] assert events[0].url == "/auth/reset" resetmsg = "We'll send instructions to the email provided " resetmsg += "if it's associated with an account. " resetmsg += "Please check your spam/junk folder." assert self.get_content(r) == resetmsg mail = self.read_mock_email() body = mail.get("body") assert body is not None assert mail.get("headers") is not None # Subject: is a key in the MIMEText assert f"Subject: {project_tile}: Password Reset" in mail.get("headers", "") assert f"{proto}://localhost/public/reset/" in body token = self.get_token_from_body(body) assert token is not None # Do password reset r = client.put(f"{AUTH_URI}/reset/thisisatoken", json={}) # this token is not valid assert r.status_code == 400 # Check if token is valid r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 204 # Token is still valid because no password still sent r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 204 # Missing information data = { "new_password": BaseAuthentication.default_password, } r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 assert self.get_content(r) == "Invalid password" data = { "password_confirm": BaseAuthentication.default_password, } r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 assert self.get_content(r) == "Invalid password" # Request with old password data = { "new_password": BaseAuthentication.default_password, "password_confirm": BaseAuthentication.default_password, } r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 409 error = "The new password cannot match the previous password" assert self.get_content(r) == error min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999) # Password too short data["new_password"] = faker.password(min_pwd_len - 1) data["password_confirm"] = faker.password(min_pwd_len - 1) r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 data["password_confirm"] = data["new_password"] r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 data["new_password"] = faker.password(min_pwd_len, strong=True) data["password_confirm"] = faker.password(min_pwd_len, strong=True) r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 assert self.get_content(r) == "New password does not match with confirmation" new_pwd = faker.password(min_pwd_len, strong=True) data["new_password"] = new_pwd data["password_confirm"] = new_pwd r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 200 # 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 self.do_login(client, None, None, status_code=401) headers, _ = self.do_login(client, None, new_pwd) # Token is no longer valid r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Restore the default password if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp(BaseAuthentication.default_user) data["password"] = new_pwd data["new_password"] = BaseAuthentication.default_password data["password_confirm"] = data["new_password"] r = client.put(f"{AUTH_URI}/profile", json=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 self.do_login(client, None, new_pwd, status_code=401) self.do_login(client, None, None) # Token created for another user token = self.get_crafted_token("r") r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Token created for another user token = self.get_crafted_token("r", wrong_algorithm=True) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Token created for another user token = self.get_crafted_token("r", wrong_secret=True) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) uuid = response.get("uuid") token = self.get_crafted_token("x", user_id=uuid) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # token created for the correct user, but from outside the system!! token = self.get_crafted_token("r", user_id=uuid) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Immature token token = self.get_crafted_token("r", user_id=uuid, immature=True) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Expired token token = self.get_crafted_token("r", user_id=uuid, expired=True) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token: this request is expired"
def test_password_management(self, faker: Faker) -> None: # Always enable during core tests if not Connector.check_availability( "authentication"): # pragma: no cover log.warning("Skipping authentication test: service not available") return auth = Connector.get_authentication_instance() min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999) pwd = faker.password(min_pwd_len - 1) ret_val, ret_text = auth.verify_password_strength(pwd, pwd) assert not ret_val assert ret_text == "The new password cannot match the previous password" pwd = faker.password(min_pwd_len - 1) old_pwd = faker.password(min_pwd_len) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val error = f"Password is too short, use at least {min_pwd_len} characters" assert ret_text == error pwd = faker.password(min_pwd_len, low=False, up=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val assert ret_text == "Password is too weak, missing lower case letters" pwd = faker.password(min_pwd_len, low=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val assert ret_text == "Password is too weak, missing upper case letters" pwd = faker.password(min_pwd_len, low=True, up=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val assert ret_text == "Password is too weak, missing numbers" pwd = faker.password(min_pwd_len, low=True, up=True, digits=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val assert ret_text == "Password is too weak, missing special characters" pwd = faker.password(min_pwd_len, low=True, up=True, digits=True, symbols=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert ret_val assert ret_text is None # How to retrieve a generic user? user = None pwd = faker.password(min_pwd_len - 1) try: auth.change_password(user, pwd, None, None) pytest.fail("None password!") # pragma: no cover except RestApiException as e: assert e.status_code == 400 assert str(e) == "Missing new password" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: auth.change_password(user, pwd, pwd, None) pytest.fail("None password!") # pragma: no cover except RestApiException as e: assert e.status_code == 400 assert str(e) == "Missing password confirmation" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: # wrong confirmation auth.change_password(user, pwd, pwd, faker.password(strong=True)) pytest.fail("wrong password confirmation!?") # pragma: no cover except RestApiException as e: assert e.status_code == 409 assert str(e) == "Your password doesn't match the confirmation" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: auth.change_password(user, pwd, pwd, pwd) pytest.fail("Password strength not verified") # pragma: no cover except RestApiException as e: assert e.status_code == 409 assert str( e) == "The new password cannot match the previous password" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: # the first password parameter is only checked for new password strenght # i.e. is verified password != newpassword # pwd validity will be checked once completed checks on new password # => a random current password is ok here auth.change_password(user, faker.password(), pwd, pwd) pytest.fail("Password strength not verified") # pragma: no cover except RestApiException as e: assert e.status_code == 409 assert ( str(e) == f"Password is too short, use at least {min_pwd_len} characters" ) except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: auth.verify_totp(None, None) # type: ignore pytest.fail("NULL totp accepted!") # pragma: no cover except RestApiException as e: assert e.status_code == 401 assert str(e) == "Verification code is missing" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") auth = Connector.get_authentication_instance() pwd1 = faker.password(strong=True) pwd2 = faker.password(strong=True) hash_1 = auth.get_password_hash(pwd1) assert len(hash_1) > 0 assert hash_1 != auth.get_password_hash(pwd2) try: auth.get_password_hash("") pytest.fail("Hashed a empty password!") # pragma: no cover except RestApiException as e: assert e.status_code == 401 assert str(e) == "Invalid password" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: auth.get_password_hash(None) pytest.fail("Hashed a None password!") # pragma: no cover except RestApiException as e: assert e.status_code == 401 assert str(e) == "Invalid password" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") assert auth.verify_password(pwd1, hash_1) try: auth.verify_password(None, hash_1) # type: ignore pytest.fail("Hashed a None password!") # pragma: no cover except TypeError: pass except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") assert not auth.verify_password(pwd1, None) # type: ignore assert not auth.verify_password(None, None) # type: ignore
def test_bot() -> None: if not Env.get_bool("TELEGRAM_ENABLE"): log.warning("Skipping BOT tests: service not available") return runner = CliRunner() start_timeout(3) try: runner.invoke(cli.bot, []) except Timeout: # pragma: no cover pass stop_timeout() from restapi.services.telegram import bot # Your API ID, hash and session string here # How to generate StringSessions: # https://docs.telethon.dev/en/latest/concepts/sessions.html#string-sessions api_id = Env.get_int("TELEGRAM_APP_ID") api_hash = Env.get("TELEGRAM_APP_HASH", "") or None session_str = Env.get("TELETHON_SESSION", "") or None botname = Env.get("TELEGRAM_BOTNAME", "") or None # use TelegramClient as a type once released the typed version 2 (issue #1195) async def send_command(client: Any, command: str) -> str: await client.send_message(botname, command) sleep(1) messages = await client.get_messages(botname) # TelegramClient is not typed => message is Any return messages[0].message # type: ignore async def test() -> None: client = TelegramClient(StringSession(session_str), api_id, api_hash) await client.start() message = await send_command(client, "/me") assert re.match(r"^Hello .*, your Telegram ID is [0-9]+", message) message = await send_command(client, "/invalid") assert message == "Invalid command, ask for /help" message = await send_command(client, "/help") assert "Available Commands:" in message assert "- /help print this help" in message assert "- /me info about yourself" in message assert "- /status get server status" in message assert "- /monitor get server monitoring stats" in message # commands requiring APIs can only be tested in PRODUCTION MODE if PRODUCTION: message = await send_command(client, "/status") assert message == "Server is alive" message = await send_command(client, "/monitor") assert message == "Please select the type of monitor" message = await send_command(client, "/monitor x") assert message == "Please select the type of monitor" message = await send_command(client, "/monitor disk") error = "Missing credentials in headers, e.g. Authorization: 'Bearer TOKEN'" assert error in message message = await send_command(client, "/monitor disk 2") assert message == "Too many inputs" # # ############################# # # # TEST USER # # # ############################# # bot.users = bot.admins bot.admins = [] message = await send_command(client, "/me") assert message == PERMISSION_DENIED message = await send_command(client, "/invalid") assert message == "Invalid command, ask for /help" message = await send_command(client, "/help") assert "Available Commands:" in message assert "- /help print this help" in message assert "- /me info about yourself" in message assert "- /status get server status" in message assert "- /monitor get server monitoring stats" in message # commands requiring APIs can only be tested in PRODUCTION MODE if PRODUCTION: message = await send_command(client, "/status") assert message == "Server is alive" message = await send_command(client, "/monitor") assert message == PERMISSION_DENIED # # ############################# # # # TEST UNAUTHORIZED # # # ############################# # bot.admins = [] bot.users = [] message = await send_command(client, "/me") assert message == PERMISSION_DENIED message = await send_command(client, "/invalid") assert message == PERMISSION_DENIED message = await send_command(client, "/help") assert message == PERMISSION_DENIED message = await send_command(client, "/status") assert message == PERMISSION_DENIED message = await send_command(client, "/monitor") assert message == PERMISSION_DENIED asyncio.run(test()) bot.shutdown()
import time from faker import Faker from restapi.config import PRODUCTION from restapi.env import Env from restapi.services.authentication import BaseAuthentication from restapi.tests import AUTH_URI, BaseTests, FlaskClient from restapi.utilities.logs import OBSCURE_VALUE, Events, log max_login_attempts = BaseAuthentication.MAX_LOGIN_ATTEMPTS ban_duration = Env.get_int("AUTH_LOGIN_BAN_TIME", 10) BAN_MESSAGE = ("Sorry, this account is temporarily blocked " + "due to the number of failed login attempts.") if max_login_attempts == 0: class TestApp1(BaseTests): 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)