Пример #1
0
    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
Пример #2
0
def test_destroy() -> None:

    # Only executed if tests are run with --destroy flag
    if os.getenv("TEST_DESTROY_MODE", "0") != "1":
        log.info("Skipping destroy test, TEST_DESTROY_MODE not enabled")
        return

    # Always enable during core tests
    if not Connector.check_availability("authentication"):  # pragma: no cover
        log.warning("Skipping authentication test: service not available")
        return

    # if Connector.check_availability("sqlalchemy"):
    #     sql = sqlalchemy.get_instance()
    #     # Close previous connections, otherwise the new create_app will hang
    #     sql.session.remove()
    #     sql.session.close_all()

    auth = Connector.get_authentication_instance()

    user = auth.get_user(username=BaseAuthentication.default_user)
    assert user is not None

    create_app(mode=ServerModes.DESTROY)

    try:
        auth = Connector.get_authentication_instance()
        user = auth.get_user(username=BaseAuthentication.default_user)
        assert user is None
    except ServiceUnavailable:
        pass
Пример #3
0
def test_pushpin(app: Flask) -> None:

    if not Connector.check_availability(CONNECTOR):

        try:
            obj = connector.get_instance()
            pytest.fail("No exception raised")  # pragma: no cover
        except ServiceUnavailable:
            pass
        log.warning("Skipping {} tests: service not available", CONNECTOR)
        return None

    log.info("Executing {} tests", CONNECTOR)

    try:
        connector.get_instance(host="invalidhostname", port=123)
        pytest.fail(
            "No exception raised on unavailable service")  # pragma: no cover
    except ServiceUnavailable:
        pass

    obj = connector.get_instance()
    assert obj is not None

    obj.disconnect()

    # a second disconnect should not raise any error
    obj.disconnect()

    # Create new connector with short expiration time
    obj = connector.get_instance(expiration=2, verification=1)
    obj_id = id(obj)

    # Connector is expected to be still valid
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) == obj_id

    time.sleep(1)

    # The connection should have been checked and should be still valid
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) == obj_id

    time.sleep(1)

    # Connection should have been expired and a new connector been created
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) != obj_id

    assert obj.is_connected()
    obj.disconnect()
    assert not obj.is_connected()

    # ... close connection again ... nothing should happens
    obj.disconnect()

    with connector.get_instance() as obj:
        assert obj is not None
Пример #4
0
    def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:

        neo4j_enabled = Connector.check_availability("neo4j")
        sqlalchemy_enabled = Connector.check_availability("sqlalchemy")
        if neo4j_enabled:
            from neomodel import db as neo4j_db

        if sqlalchemy_enabled:
            # thanks to connectors cache this should always match the
            # same instance that will be used from inside the endpoint
            from restapi.connectors import sqlalchemy

            alchemy_db = sqlalchemy.get_instance()

        try:

            if neo4j_enabled:
                neo4j_db.begin()

            # Transaction is already open...
            # if sqlalchemy_enabled:
            #     pass

            out = func(self, *args, **kwargs)

            if neo4j_enabled:
                neo4j_db.commit()

            if sqlalchemy_enabled:
                alchemy_db.session.commit()

            return out
        except Exception as e:
            log.debug("Rolling backend database transaction")
            try:

                if neo4j_enabled:
                    neo4j_db.rollback()

                if sqlalchemy_enabled:
                    alchemy_db.session.rollback()

            except Exception as sub_ex:  # pragma: no cover
                log.warning("Exception raised during rollback: {}", sub_ex)
            raise e
Пример #5
0
def send_notification(
    subject: str,
    template: str,
    # if None will be sent to the administrator
    to_address: Optional[str] = None,
    data: Optional[Dict[str, Any]] = None,
    user: Optional[User] = None,
    send_async: bool = False,
) -> bool:

    # Always enabled during tests
    if not Connector.check_availability("smtp"):  # pragma: no cover
        return False

    title = get_project_configuration("project.title", default="Unkown title")
    reply_to = Env.get("SMTP_NOREPLY", Env.get("SMTP_ADMIN", ""))

    if data is None:
        data = {}

    data.setdefault("project", title)
    data.setdefault("reply_to", reply_to)

    if user:
        data.setdefault("username", user.email)
        data.setdefault("name", user.name)
        data.setdefault("surname", user.surname)

    html_body, plain_body = get_html_template(template, data)

    if not html_body:  # pragma: no cover
        log.error("Can't load {}", template)
        return False

    subject = f"{title}: {subject}"

    if send_async:
        Mail.send_async(
            subject=subject,
            body=html_body,
            to_address=to_address,
            plain_body=plain_body,
        )
        return False

    smtp_client = smtp.get_instance()
    return smtp_client.send(
        subject=subject,
        body=html_body,
        to_address=to_address,
        plain_body=plain_body,
    )
Пример #6
0
    def get_instance(app: Flask) -> FlaskCache:  # type: ignore

        # This check prevent KeyError raised during tests
        # Exactly as reported here:
        # https://github.com/sh4nks/flask-caching/issues/191
        if not hasattr(mem, "cache"):

            cache_config = Cache.get_config(
                use_redis=Connector.check_availability("redis"))
            mem.cache = FlaskCache(config=cache_config)

        mem.cache.init_app(app)

        return mem.cache
Пример #7
0
        def do_queries(self, value: str) -> None:

            neo4j_enabled = Connector.check_availability("neo4j")
            sql_enabled = Connector.check_availability("sqlalchemy")
            mysql_enabled = sql_enabled and sqlalchemy.SQLAlchemy.is_mysql()
            postgres_enabled = sql_enabled and not sqlalchemy.SQLAlchemy.is_mysql(
            )

            # This is just a stub... to be completed
            if neo4j_enabled:
                graph = neo4j.get_instance()

                graph.cypher(
                    "MATCH (g: Group) WHERE g.shortname = $value return g.shortname",
                    value=value,
                )

                graph.Group.nodes.get_or_none(shortname=value)

            elif postgres_enabled:
                sql = sqlalchemy.get_instance()

                t = sqlalchemy.text(
                    'SELECT * FROM "group" WHERE shortname = :value')
                sql.db.engine.execute(t, value=value)

                sql.Group.query.filter_by(shortname=value).first()

            elif mysql_enabled:
                sql = sqlalchemy.get_instance()

                t = sqlalchemy.text(
                    "SELECT * FROM `group` WHERE shortname = :value")
                sql.db.engine.execute(t, value=value)

                sql.Group.query.filter_by(shortname=value).first()
Пример #8
0
    def test_depends_on(self, client: FlaskClient) -> None:

        if Connector.check_availability("neo4j"):

            r = client.get(f"{API_URI}/tests/depends_on/neo4j")
            assert r.status_code == 200

            r = client.get(f"{API_URI}/tests/depends_on_not/neo4j")
            assert r.status_code == 404

        else:

            r = client.get(f"{API_URI}/tests/depends_on/neo4j")
            assert r.status_code == 404

            r = client.get(f"{API_URI}/tests/depends_on_not/neo4j")
            assert r.status_code == 200
Пример #9
0
def test_destroy() -> None:

    auth = Connector.get_authentication_instance()

    user = auth.get_user(username=BaseAuthentication.default_user)
    assert user is not None

    create_app(mode=ServerModes.DESTROY)

    if Connector.check_availability("sqlalchemy"):
        with pytest.raises(ServiceUnavailable):
            auth = Connector.get_authentication_instance()
            user = auth.get_user(username=BaseAuthentication.default_user)
    else:
        auth = Connector.get_authentication_instance()
        user = auth.get_user(username=BaseAuthentication.default_user)
        assert user is None
Пример #10
0
    def test_ip_management(self) -> 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()

        ip_data = auth.localize_ip("8.8.8.8")

        assert ip_data is not None
        # I don't know if this tests will be stable...
        assert ip_data == "United States"

        assert auth.localize_ip("8.8.8.8, 4.4.4.4") is None
Пример #11
0
def verify(services):
    """Verify connected service"""

    if len(services) == 0:
        log.warning("Empty list of services, nothing to be verified.")
        log.info("Provide list of services by using --services option")

    for service in services:

        if not Connector.check_availability(service):
            print_and_exit("Service {} not detected", service)

        log.info("Verifying service: {}", service)
        variables = Connector.services.get(service, {})
        host, port = get_service_address(variables, "host", "port", service)
        wait_socket(host, port, service)

    log.info("Completed successfully")
Пример #12
0
def test_init() -> None:

    auth = Connector.get_authentication_instance()
    if Connector.authentication_service == "sqlalchemy":
        # Re-init does not work with MySQL due to issues with previous connections
        # Considering that:
        # 1) this is a workaround to test the initialization
        #       (not the normal workflow used by the application)
        # 2) the init is already tested with any other DB, included postgres
        # 3) MySQL is not used by any project
        # => there is no need to go crazy in debugging this issue!
        if auth.db.is_mysql():  # type: ignore
            return

        # sql = sqlalchemy.get_instance()

    if Connector.check_availability("sqlalchemy"):
        # Prevents errors like:
        # sqlalchemy.exc.ResourceClosedError: This Connection is closed
        Connector.disconnect_all()

        # sql = sqlalchemy.get_instance()
        # # Close previous connections, otherwise the new create_app will hang
        # sql.session.remove()
        # sql.session.close_all()

    try:
        create_app(mode=ServerModes.INIT)
        # This is only a rough retry to prevent random errors from sqlalchemy
    except Exception:  # pragma: no cover
        create_app(mode=ServerModes.INIT)

    auth = Connector.get_authentication_instance()
    try:
        user = auth.get_user(username=BaseAuthentication.default_user)
    # SqlAlchemy sometimes can raise an:
    # AttributeError: 'NoneType' object has no attribute 'twophase'
    # due to the multiple app created... should be an issue specific of this test
    # In that case... simply retry.
    except AttributeError:  # pragma: no cover
        user = auth.get_user(username=BaseAuthentication.default_user)

    assert user is not None
Пример #13
0
def verify(service: str) -> None:
    """Verify if a service is connected"""

    if not Connector.check_availability(service):
        print_and_exit("Service {} not detected", service)

    log.info("Verifying service: {}", service)
    variables = Connector.services.get(service, {})
    host, port = get_service_address(variables, "host", "port", service)
    if host != "nohost":
        wait_socket(host, port, service)

    connector_module = Connector.get_module(service, BACKEND_PACKAGE)
    if not connector_module:  # pragma: no cover
        print_and_exit("Connector {} not detected", service)

    c = connector_module.get_instance()
    log.info("{} successfully authenticated on {}", service,
             c.variables.get("host", service))
Пример #14
0
def mark_task_as_failed(self: Any, name: str,
                        exception: Exception) -> NoReturn:
    if TESTING:
        self.request.id = "fixed-id"
        self.request.task = name

    task_id = self.request.id
    task_name = self.request.task
    arguments = str(self.request.args)

    # Removing username and password from urls in error stack
    clean_error_stack = ""
    for line in traceback.format_exc().split("\n"):
        clean_error_stack += f"{obfuscate_url(line)}\n"

    log.error("Celery task {} ({}) failed", task_id, task_name)
    log.error("Failed task arguments: {}", arguments[0:256])
    log.error("Task error: {}", clean_error_stack)

    if Connector.check_availability("smtp"):
        log.info("Sending error report by email", task_id, task_name)
        send_celery_error_notification(task_id, task_name, arguments,
                                       clean_error_stack, -1)
    self.update_state(
        state=states.FAILURE,
        meta={
            "exc_type": type(exception).__name__,
            "exc_message": traceback.format_exc().split("\n"),
            # 'custom': '...'
        },
    )
    self.send_event(
        "task-failed",
        # Retry sending the message if the connection is lost
        retry=True,
        exception=str(exception),
        traceback=traceback.format_exc(),
    )

    raise Ignore(str(exception))
Пример #15
0
def test_init() -> None:

    # Only executed if tests are run with --destroy flag
    if os.getenv("TEST_DESTROY_MODE", "0") != "1":
        log.info("Skipping destroy test, TEST_DESTROY_MODE not enabled")
        return

    # 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()
    if Connector.authentication_service == "sqlalchemy":
        # Re-init does not work with MySQL due to issues with previous connections
        # Considering that:
        # 1) this is a workaround to test the initialization
        #       (not the normal workflow used by the application)
        # 2) the init is already tested with any other DB, included postgres
        # 3) MySQL is not used by any project
        # => there is no need to go crazy in debugging this issue!
        if auth.db.is_mysql():  # type: ignore
            return

        # sql = sqlalchemy.get_instance()

    create_app(mode=ServerModes.INIT)

    auth = Connector.get_authentication_instance()
    try:
        user = auth.get_user(username=BaseAuthentication.default_user)
    # SqlAlchemy sometimes can raise an:
    # AttributeError: 'NoneType' object has no attribute 'twophase'
    # due to the multiple app created... should be an issue specific of this test
    # In that case... simply retry.
    except AttributeError:  # pragma: no cover
        user = auth.get_user(username=BaseAuthentication.default_user)

    assert user is not None
Пример #16
0
def mark_task_as_retriable(self: Any, name: str, exception: Exception,
                           MAX_RETRIES: int) -> NoReturn:
    if TESTING:
        self.request.id = "fixed-id"
        self.request.task = name
        self.request.retries = 0

    task_id = self.request.id
    task_name = self.request.task
    arguments = str(self.request.args)
    retry_num = 1 + self.request.retries

    # All retries attempts failed,
    # the error will be converted to permanent
    if retry_num > MAX_RETRIES:
        log.critical("MAX retries reached")
        mark_task_as_failed(self=self, name=name, exception=exception)

    # Removing username and password from urls in error stack
    clean_error_stack = ""
    for line in traceback.format_exc().split("\n"):
        clean_error_stack += f"{obfuscate_url(line)}\n"

    log.warning(
        "Celery task {} ({}) failed due to: {}, "
        "but will be retried (fail #{}/{})",
        task_id,
        task_name,
        exception,
        retry_num,
        MAX_RETRIES,
    )

    if Connector.check_availability("smtp"):
        log.info("Sending error report by email", task_id, task_name)
        send_celery_error_notification(task_id, task_name, arguments,
                                       clean_error_stack, retry_num)
    raise exception
Пример #17
0
    def wrapper(self, *args, **kwargs):

        try:
            return func(self, *args, **kwargs)

        except BaseException:

            task_id = self.request.id
            task_name = self.request.task

            log.error("Celery task {} failed ({})", task_id, task_name)
            arguments = str(self.request.args)
            log.error("Failed task arguments: {}", arguments[0:256])
            log.error("Task error: {}", traceback.format_exc())

            if Connector.check_availability("smtp"):
                log.info("Sending error report by email", task_id, task_name)

                body = f"""
Celery task {task_id} failed

Name: {task_name}

Arguments: {self.request.args}

Error: {traceback.format_exc()}
"""

                project = get_project_configuration(
                    "project.title",
                    default="Unkown title",
                )
                subject = f"{project}: task {task_name} failed"
                from restapi.connectors import smtp

                smtp_client = smtp.get_instance()
                smtp_client.send(body, subject)
Пример #18
0
def admin_user_input(request: FlaskRequest, is_post: bool) -> Type[Schema]:

    is_admin = HTTPTokenAuth.is_session_user_admin(request, auth)

    attributes: MarshmallowSchema = {}
    if is_post:
        # This is because Email is not typed on marshmallow
        attributes["email"] = fields.Email(  # type: ignore
            required=is_post,
            validate=validate.Length(max=100))

    attributes["name"] = fields.Str(
        required=is_post,
        validate=validate.Length(min=1),
        metadata={"label": "First Name"},
    )
    attributes["surname"] = fields.Str(
        required=is_post,
        validate=validate.Length(min=1),
        metadata={"label": "Last Name"},
    )

    attributes["password"] = fields.Str(
        required=is_post,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
        metadata={"password": True},
    )

    if Connector.check_availability("smtp"):
        attributes["email_notification"] = fields.Bool(
            metadata={"label": "Notify password by email"})

    attributes["is_active"] = fields.Bool(
        dump_default=True,
        required=False,
        metadata={"label": "Activate user"},
    )

    roles = {r.name: r.description for r in auth.get_roles()}
    if not is_admin and RoleEnum.ADMIN.value in roles:
        roles.pop(RoleEnum.ADMIN.value)

    attributes["roles"] = fields.List(
        fields.Str(validate=validate.OneOf(
            choices=[r for r in roles.keys()],
            labels=[r for r in roles.values()],
        )),
        dump_default=[auth.default_role],
        required=False,
        unique=True,
        metadata={
            "label": "Roles",
            "description": "",
            "extra_descriptions": auth.role_descriptions,
        },
    )

    group_keys = []
    group_labels = []

    for g in auth.get_groups():
        group_keys.append(g.uuid)
        group_labels.append(f"{g.shortname} - {g.fullname}")

    if len(group_keys) == 1:
        default_group = group_keys[0]
    else:
        default_group = None

    attributes["group"] = fields.Str(
        required=is_post,
        dump_default=default_group,
        validate=validate.OneOf(choices=group_keys, labels=group_labels),
        metadata={
            "label": "Group",
            "description": "The group to which the user belongs",
        },
    )

    attributes["expiration"] = fields.DateTime(
        required=False,
        allow_none=True,
        metadata={
            "label": "Account expiration",
            "description": "This user will be blocked after this date",
        },
    )

    if custom_fields := mem.customizer.get_custom_input_fields(
            request=request, scope=mem.customizer.ADMIN):
        attributes.update(custom_fields)
Пример #19
0
import pytest
import pytz
from faker import Faker
from flask import Flask
from neo4j.exceptions import CypherSyntaxError

from restapi.connectors import Connector
from restapi.connectors import neo4j as connector
from restapi.connectors.neo4j.parser import DataDump, NodeDump, RelationDump
from restapi.exceptions import ServiceUnavailable
from restapi.services.authentication import BaseAuthentication
from restapi.tests import API_URI, BaseTests, FlaskClient
from restapi.utilities.logs import log

CONNECTOR = "neo4j"
CONNECTOR_AVAILABLE = Connector.check_availability(CONNECTOR)


@pytest.mark.skipif(CONNECTOR_AVAILABLE,
                    reason=f"This test needs {CONNECTOR} to be not available")
def test_no_neo4j() -> None:

    with pytest.raises(ServiceUnavailable):
        connector.get_instance()

    log.warning("Skipping {} tests: service not available", CONNECTOR)
    return None


@pytest.mark.skipif(not CONNECTOR_AVAILABLE,
                    reason=f"This test needs {CONNECTOR} to be available")
Пример #20
0
def verify_token_is_not_valid(auth: BaseAuthentication,
                              token: str,
                              ttype: Optional[str] = None) -> None:
    unpacked_token = auth.verify_token(token, token_type=ttype)
    assert not unpacked_token[0]
    assert unpacked_token[1] is None
    assert unpacked_token[2] is None
    assert unpacked_token[3] is None

    with pytest.raises(Exception):
        auth.verify_token(token, token_type=ttype, raiseErrors=True)


@pytest.mark.skipif(
    not Connector.check_availability("authentication"),
    reason="This test needs authentication to be available",
)
class TestApp(BaseTests):
    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)
Пример #21
0
def getInputSchema(request, is_post):

    # as defined in Marshmallow.schema.from_dict
    attributes: Dict[str, Union[fields.Field, type]] = {}
    if is_post:
        attributes["email"] = fields.Email(required=is_post)

    attributes["name"] = fields.Str(required=is_post,
                                    validate=validate.Length(min=1))
    attributes["surname"] = fields.Str(required=is_post,
                                       validate=validate.Length(min=1))

    attributes["password"] = fields.Str(
        required=is_post,
        password=True,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
    )

    if Connector.check_availability("smtp"):
        attributes["email_notification"] = fields.Bool(
            label="Notify password by email")

    attributes["is_active"] = fields.Bool(label="Activate user",
                                          default=True,
                                          required=False)

    roles = {r.name: r.description for r in auth.get_roles()}

    attributes["roles"] = AdvancedList(
        fields.Str(validate=validate.OneOf(
            choices=[r for r in roles.keys()],
            labels=[r for r in roles.values()],
        )),
        required=False,
        label="Roles",
        description="",
        unique=True,
        multiple=True,
    )

    group_keys = []
    group_labels = []

    for g in auth.get_groups():
        group_keys.append(g.uuid)
        group_labels.append(f"{g.shortname} - {g.fullname}")

    if len(group_keys) == 1:
        default_group = group_keys[0]
    else:
        default_group = None

    attributes["group"] = fields.Str(
        label="Group",
        description="The group to which the user belongs",
        required=is_post,
        default=default_group,
        validate=validate.OneOf(choices=group_keys, labels=group_labels),
    )

    attributes["expiration"] = fields.DateTime(
        required=False,
        allow_none=True,
        label="Account expiration",
        description="This user will be blocked after this date",
    )

    if custom_fields := mem.customizer.get_custom_input_fields(
            request=request, scope=mem.customizer.ADMIN):
        attributes.update(custom_fields)
Пример #22
0
class TestApp(BaseTests):
    def test_inputs(self, client: FlaskClient) -> None:

        # This test verifies that buildData is always able to randomly create
        # valid inputs for endpoints with inputs defined by marshamallow schemas
        schema = self.get_dynamic_input_schema(client, "tests/inputs", {})
        # Expected number of fields
        assert len(schema) == 14
        for field in schema:

            # Always in the schema
            assert "key" in field
            assert "type" in field
            assert "label" in field
            assert "description" in field
            assert "required" in field

            # Other optional keys
            # - default
            # - min
            # - max
            # - options
            # - schema in case of nested fields

        field = schema[0]
        assert len(field) == 6  # 5 mandatory fields + min
        assert field["key"] == "mystr"
        assert field["type"] == "string"
        # This is the default case: both label and description are not explicitly set
        # if key is lower-cased the corrisponding label will be titled
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]
        assert "min" in field
        assert field["min"] == 4
        assert "max" not in field

        field = schema[1]
        assert len(field) == 5  # 5 mandatory fields, min and max not set
        assert field["key"] == "MYDATE"
        assert field["type"] == "date"
        # Here the key is not lower cased and the label is not explicitly set
        # So the label will exactly match the key (without additiona of .title)
        assert field["label"] == field["key"]
        assert field["label"] != field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]

        field = schema[2]
        assert len(field) == 7  # 5 mandatory fields + min + max
        assert field["key"] == "MYDATETIME"
        assert field["type"] == "datetime"
        # Here the key is not lower cased and the label is not explicitly set
        # So the label will exactly match the key (without additiona of .title)
        assert field["label"] == field["key"]
        assert field["label"] != field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]
        assert "min" in field
        assert "max" in field

        field = schema[3]
        assert len(field) == 7  # 5 mandatory fields + min + max
        assert field["key"] == "myint_exclusive"
        assert field["type"] == "int"
        # Here an explicit label is defined but not a description, so is == to the label
        assert field["label"] != field["key"]
        assert field["label"] != field["key"].title()
        assert field["label"] == "Int exclusive field"
        assert field["description"] == field["label"]
        assert field["required"]
        assert "min" in field
        assert field["min"] == 2
        assert "max" in field
        assert field["max"] == 9

        field = schema[4]
        assert len(field) == 7  # 5 mandatory fields + min + max
        assert field["key"] == "myint_inclusive"
        assert field["type"] == "int"
        # Here both label and description are explicitly set
        assert field["label"] != field["key"]
        assert field["label"] != field["key"].title()
        assert field["label"] == "Int inclusive field"
        assert field["description"] != field["label"]
        assert field["description"] == "This field accepts values in a defined range"
        assert field["required"]
        assert "min" in field
        assert field["min"] == 1
        assert "max" in field
        assert field["max"] == 10

        field = schema[5]
        assert len(field) == 6  # 5 mandatory fields + options
        assert field["key"] == "myselect"
        assert field["type"] == "string"
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]
        assert "options" in field
        assert isinstance(field["options"], dict)
        assert len(field["options"]) == 2
        assert "a" in field["options"]
        assert "b" in field["options"]
        # The field defines labels and keys for all options
        assert field["options"]["a"] == "A"
        assert field["options"]["b"] == "B"

        field = schema[6]
        assert len(field) == 6  # 5 mandatory fields + options
        assert field["key"] == "myselect2"
        assert field["type"] == "string"
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]
        assert "options" in field
        assert isinstance(field["options"], dict)
        assert len(field["options"]) == 2
        assert "a" in field["options"]
        assert "b" in field["options"]
        # The field wrongly defines labels, so are defaulted to keys
        assert field["options"]["a"] == "a"
        assert field["options"]["b"] == "b"

        field = schema[7]
        assert len(field) == 6  # 5 mandatory fields + max
        assert field["key"] == "mymaxstr"
        assert field["type"] == "string"
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]
        assert "min" not in field
        assert "max" in field
        assert field["max"] == 7

        field = schema[8]
        assert len(field) == 7  # 5 mandatory fields + min + max
        assert field["key"] == "myequalstr"
        assert field["type"] == "string"
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]
        assert "min" in field
        assert "max" in field
        assert field["min"] == 6
        assert field["max"] == 6

        field = schema[9]
        assert len(field) == 6  # 5 mandatory fields + schema
        assert field["key"] == "mynested"
        assert field["type"] == "nested"
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]
        assert "schema" in field

        field = schema[10]
        assert len(field) == 6  # 5 mandatory fields + schema
        assert field["key"] == "mynullablenested"
        assert field["type"] == "nested"
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]
        assert "schema" in field

        field = schema[11]
        assert len(field) == 5  # 5 mandatory fields
        assert field["key"] == "mylist"
        assert field["type"] == "string[]"
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]

        field = schema[12]
        assert len(field) == 5  # 5 mandatory fields
        assert field["key"] == "mylist2"
        # The list is defined as List(CustomInt) and CustomInt is resolved as int
        assert field["type"] == "int[]"
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]

        field = schema[13]
        assert len(field) == 5  # 5 mandatory fields
        assert field["key"] == "mylist3"
        # The type is key[] ... should be something more explicative like FieldName[]
        # assert field["type"] == "CustomGenericField[]"
        assert field["type"] == "mylist3[]"
        assert field["label"] == field["key"].title()
        assert field["description"] == field["label"]
        assert field["required"]

        data = self.buildData(schema)

        # mylist3 is a list of custom field, buildData can't automatically set a value
        assert "mylist3" not in data
        data["mylist3"] = orjson.dumps(["mycustominputvalue"]).decode("UTF8")

        r = client.post(f"{API_URI}/tests/inputs", json=data)
        assert r.status_code == 204

        # This is to verify that access_token, if provided is excluded from parameters
        # And do not raise any ValidationError for unknown input

        if Env.get_bool("AUTH_ENABLE"):
            _, token = self.do_login(client, None, None)
            data["access_token"] = token
            r = client.post(f"{API_URI}/tests/inputs", json=data)
            assert r.status_code == 204

        # This is to verify that unknown inputs raise a ValidationError
        data["unknown"] = "input"
        r = client.post(f"{API_URI}/tests/inputs", json=data)
        assert r.status_code == 400

    @pytest.mark.skipif(
        not Connector.check_availability("neo4j"),
        reason="This test needs neo4j to be available",
    )
    def test_neo4j_inputs(self, client: FlaskClient) -> None:

        headers, _ = self.do_login(client, None, None)
        schema = self.get_dynamic_input_schema(client, "tests/neo4jinputs", headers)
        assert len(schema) == 1

        field = schema[0]
        assert field["key"] == "choice"
        # This is because the Neo4jChoice field is not completed for deserialization
        # It is should be automatically translated into a select, with options by
        # including a validation OneOf
        assert "options" not in field

        r = client.post(
            f"{API_URI}/tests/neo4jinputs", json={"choice": "A"}, headers=headers
        )
        assert r.status_code == 200
        response = self.get_content(r)
        assert isinstance(response, dict)
        assert "choice" in response
        assert "key" in response["choice"]
        assert "description" in response["choice"]
        assert response["choice"]["key"] == "A"
        assert response["choice"]["description"] == "AAA"

        assert "relationship_count" in response
        assert isinstance(response["relationship_count"], int)
        assert response["relationship_count"] > 0

        assert "relationship_single" in response
        assert isinstance(response["relationship_single"], dict)
        assert "uuid" in response["relationship_single"]

        assert "relationship_many" in response
        assert isinstance(response["relationship_many"], list)
        assert len(response["relationship_many"]) > 0
        assert isinstance(response["relationship_many"][0], dict)
        assert "token_type" in response["relationship_many"][0]

        r = client.post(
            f"{API_URI}/tests/neo4jinputs", json={"choice": "B"}, headers=headers
        )
        assert r.status_code == 200
        response = self.get_content(r)
        assert isinstance(response, dict)
        assert "choice" in response
        assert "key" in response["choice"]
        assert "description" in response["choice"]
        assert response["choice"]["key"] == "B"
        assert response["choice"]["description"] == "BBB"

        r = client.post(
            f"{API_URI}/tests/neo4jinputs", json={"choice": "C"}, headers=headers
        )
        assert r.status_code == 200
        response = self.get_content(r)
        assert isinstance(response, dict)
        assert "choice" in response
        assert "key" in response["choice"]
        assert "description" in response["choice"]
        assert response["choice"]["key"] == "C"
        assert response["choice"]["description"] == "CCC"

        r = client.post(
            f"{API_URI}/tests/neo4jinputs", json={"choice": "D"}, headers=headers
        )
        # This should fail, but Neo4jChoice are not validated as input
        # assert r.status_code == 400
        # Since validation is not implemented, D is accepted But since it is
        # not included in the choice, the description will simply match the key
        assert r.status_code == 200
        response = self.get_content(r)
        assert isinstance(response, dict)
        assert "choice" in response
        assert "key" in response["choice"]
        assert "description" in response["choice"]
        assert response["choice"]["key"] == "D"
        assert response["choice"]["description"] == "D"
Пример #23
0
from restapi import decorators
from restapi.config import TESTING
from restapi.connectors import Connector, neo4j
from restapi.exceptions import BadRequest
from restapi.models import Neo4jChoice, Neo4jSchema, Schema, fields
from restapi.rest.definition import EndpointResource, Response
from restapi.utilities.logs import log

if TESTING and Connector.check_availability("neo4j"):

    from restapi.connectors.neo4j.models import Group, User

    CHOICES_tuple = (("A", "A"), ("B", "B"), ("C", "C"))
    CHOICES_dict = {"A": "A", "B": "B", "C": "C"}

    class Output(Schema):
        val = fields.Integer()
        created = fields.DateTime()
        modified1 = fields.DateTime()
        modified2 = fields.DateTime()
        user = Neo4jSchema(
            User,
            fields=(
                "uuid",
                "email",
                "name",
                "surname",
                "is_active",
                "last_password_change",
            ),
        )
Пример #24
0
def test_sqlalchemy(app: Flask) -> None:

    if not Connector.check_availability(CONNECTOR):

        try:
            obj = connector.get_instance()
            pytest.fail("No exception raised")  # pragma: no cover
        except ServiceUnavailable:
            pass

        log.warning("Skipping {} tests: service not available", CONNECTOR)
        return None

    log.info("Executing {} tests", CONNECTOR)

    if not connector.SQLAlchemy.is_mysql():
        try:
            connector.get_instance(host="invalidhostname", port=123)

            pytest.fail("No exception raised on unavailable service"
                        )  # pragma: no cover
        except ServiceUnavailable:
            pass

    try:
        connector.get_instance(user="******")

        pytest.fail(
            "No exception raised on unavailable service")  # pragma: no cover
    except ServiceUnavailable:
        pass

    obj = connector.get_instance()
    assert obj is not None

    try:
        obj.InvalidModel
        pytest.fail("No exception raised on InvalidModel")  # pragma: no cover
    except AttributeError as e:
        assert str(e) == "Model InvalidModel not found"

    obj.disconnect()

    # a second disconnect should not raise any error
    obj.disconnect()

    # Create new connector with short expiration time
    obj = connector.get_instance(expiration=2, verification=1)
    obj_id = id(obj)
    obj_db_id = id(obj.db)

    # Connector is expected to be still valid
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) == obj_id

    time.sleep(1)

    # The connection should have been checked and should be still valid
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) == obj_id

    time.sleep(1)

    obj = connector.get_instance(expiration=2, verification=1)
    # With alchemy the connection object remains the same...
    assert id(obj) != obj_id
    assert id(obj.db) == obj_db_id

    assert obj.is_connected()
    obj.disconnect()
    assert not obj.is_connected()

    # ... close connection again ... nothing should happens
    obj.disconnect()
Пример #25
0
    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)
Пример #26
0
import pytest
import pytz
from faker import Faker
from flask import Flask
from neo4j.exceptions import CypherSyntaxError

from restapi.connectors import Connector
from restapi.connectors import neo4j as connector
from restapi.env import Env
from restapi.exceptions import ServiceUnavailable
from restapi.tests import API_URI, BaseTests, FlaskClient
from restapi.utilities.logs import log

CONNECTOR = "neo4j"

if not Connector.check_availability(CONNECTOR):

    try:
        obj = connector.get_instance()
        pytest.fail("No exception raised")  # pragma: no cover
    except ServiceUnavailable:
        pass

    log.warning("Skipping {} tests: service not available", CONNECTOR)
# Alwas enabled during core tests
elif not Env.get_bool("TEST_CORE_ENABLED"):  # pragma: no cover
    log.warning("Skipping {} tests: only avaiable on core", CONNECTOR)
else:

    log.info("Executing {} tests", CONNECTOR)
Пример #27
0
def test_celery(app: Flask, faker: Faker) -> None:

    if not Connector.check_availability(CONNECTOR):

        try:
            obj = connector.get_instance()
            pytest.fail("No exception raised")  # pragma: no cover
        except ServiceUnavailable:
            pass

        log.warning("Skipping {} tests: service not available", CONNECTOR)
        return None

    log.info("Executing {} tests", CONNECTOR)

    obj = connector.get_instance()
    assert obj is not None

    task = obj.celery_app.send_task("test_task")

    assert task is not None
    assert task.id is not None

    if obj.variables.get("backend") == "RABBIT":
        log.warning(
            "Due to limitations on RABBIT backend task results will not be tested"
        )
    else:
        try:
            r = task.get(timeout=10)
            assert r is not None
            # This is the task output, as defined in task_template.py.j2
            assert r == "Task executed!"
            assert task.status == "SUCCESS"
            assert task.result == "Task executed!"
        except celery.exceptions.TimeoutError:  # pragma: no cover
            pytest.fail(
                f"Task timeout, result={task.result}, status={task.status}")

    if CeleryExt.CELERYBEAT_SCHEDULER is None:

        try:
            obj.get_periodic_task("does_not_exist")
            pytest.fail("get_periodic_task with unknown CELERYBEAT_SCHEDULER"
                        )  # pragma: no cover
        except AttributeError as e:
            assert str(e) == "Unsupported celery-beat scheduler: None"
        except BaseException:  # pragma: no cover
            pytest.fail("Unexpected exception raised")

        try:
            obj.delete_periodic_task("does_not_exist")
            pytest.fail(
                "delete_periodic_task with unknown CELERYBEAT_SCHEDULER"
            )  # pragma: no cover
        except AttributeError as e:
            assert str(e) == "Unsupported celery-beat scheduler: None"
        except BaseException:  # pragma: no cover
            pytest.fail("Unexpected exception raised")

        try:
            obj.create_periodic_task(name="task1",
                                     task="task.does.not.exists",
                                     every="60")
            pytest.fail(
                "create_periodic_task with unknown CELERYBEAT_SCHEDULER"
            )  # pragma: no cover
        except AttributeError as e:
            assert str(e) == "Unsupported celery-beat scheduler: None"
        except BaseException:  # pragma: no cover
            pytest.fail("Unexpected exception raised")

        try:
            obj.create_crontab_task(name="task2",
                                    task="task.does.not.exists",
                                    minute="0",
                                    hour="1")
            pytest.fail("create_crontab_task with unknown CELERYBEAT_SCHEDULER"
                        )  # pragma: no cover
        except AttributeError as e:
            assert str(e) == "Unsupported celery-beat scheduler: None"
        except BaseException:  # pragma: no cover
            pytest.fail("Unexpected exception raised")

    else:
        assert obj.get_periodic_task("does_not_exist") is None
        assert not obj.delete_periodic_task("does_not_exist")

        obj.create_periodic_task(name="task1",
                                 task="task.does.not.exists",
                                 every="60")

        assert obj.delete_periodic_task("task1")
        assert not obj.delete_periodic_task("task1")

        obj.create_periodic_task(
            name="task1_bis",
            task="task.does.not.exists",
            every="60",
            period="seconds",
            args=["a", "b", "c"],
            kwargs={
                "a": 1,
                "b": 2,
                "c": 3
            },
        )

        assert obj.delete_periodic_task("task1_bis")
        assert not obj.delete_periodic_task("task1_bis")

        # cron at 01:00
        obj.create_crontab_task(name="task2",
                                task="task.does.not.exists",
                                minute="0",
                                hour="1")

        assert obj.delete_periodic_task("task2")
        assert not obj.delete_periodic_task("task2")

        obj.create_crontab_task(
            name="task2_bis",
            task="task.does.not.exists",
            minute="0",
            hour="1",
            day_of_week="*",
            day_of_month="*",
            month_of_year="*",
            args=["a", "b", "c"],
            kwargs={
                "a": 1,
                "b": 2,
                "c": 3
            },
        )

        assert obj.delete_periodic_task("task2_bis")
        assert not obj.delete_periodic_task("task2_bis")

        if CeleryExt.CELERYBEAT_SCHEDULER == "REDIS":

            obj.create_periodic_task(
                name="task3",
                task="task.does.not.exists",
                every=60,
            )
            assert obj.delete_periodic_task("task3")

            obj.create_periodic_task(name="task4",
                                     task="task.does.not.exists",
                                     every=60,
                                     period="seconds")
            assert obj.delete_periodic_task("task4")

            obj.create_periodic_task(name="task5",
                                     task="task.does.not.exists",
                                     every=60,
                                     period="minutes")
            assert obj.delete_periodic_task("task5")

            obj.create_periodic_task(name="task6",
                                     task="task.does.not.exists",
                                     every=60,
                                     period="hours")
            assert obj.delete_periodic_task("task6")

            obj.create_periodic_task(name="task7",
                                     task="task.does.not.exists",
                                     every=60,
                                     period="days")
            assert obj.delete_periodic_task("task7")

            try:
                obj.create_periodic_task(
                    name="task8",
                    task="task.does.not.exists",
                    every="60",
                    period="years",  # type: ignore
                )
            except BadRequest as e:
                assert str(e) == "Invalid timedelta period: years"

            obj.create_periodic_task(
                name="task9",
                task="task.does.not.exists",
                every=timedelta(seconds=60),
            )
            assert obj.delete_periodic_task("task9")

            try:
                obj.create_periodic_task(
                    name="task10",
                    task="task.does.not.exists",
                    every=["60"],  # type: ignore
                )
            except AttributeError as e:
                assert str(
                    e) == "Invalid input parameter every = ['60'] (type list)"

            try:
                obj.create_periodic_task(
                    name="task11",
                    task="task.does.not.exists",
                    every="invalid",
                )
            except AttributeError as e:
                assert str(
                    e) == "Invalid input parameter every = invalid (type str)"

        else:
            obj.create_periodic_task(name="task3",
                                     task="task.does.not.exists",
                                     every="60",
                                     period="minutes")
            assert obj.delete_periodic_task("task3")

    obj.disconnect()

    # a second disconnect should not raise any error
    obj.disconnect()

    # Create new connector with short expiration time
    obj = connector.get_instance(expiration=2, verification=1)
    obj_id = id(obj)

    # Connector is expected to be still valid
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) == obj_id

    time.sleep(1)

    # The connection should have been checked and should be still valid
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) == obj_id

    time.sleep(1)

    # Connection should have been expired and a new connector been created
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) != obj_id

    assert obj.is_connected()
    obj.disconnect()
    assert not obj.is_connected()

    # ... close connection again ... nothing should happens
    obj.disconnect()

    with connector.get_instance() as obj:
        assert obj is not None

    app = create_app(mode=ServerModes.WORKER)
    assert app is not None
    from restapi.utilities.logs import LOGS_FILE

    assert os.environ["HOSTNAME"] == "backend-server"
    assert LOGS_FILE == "backend-server"

    # this decorator is expected to be used in celery context, i.e. the self reference
    # should contains a request, injected by celery. Let's mock this by injecting an
    # artificial self
    @send_errors_by_email
    def this_function_raises_exceptions(self):
        raise AttributeError("Just an exception")

    class FakeRequest:
        def __init__(self, task_id, task, args):
            self.id = task_id
            self.task = task
            self.args = args

    class FakeSelf:
        def __init__(self, task_id, task, args):
            self.request = FakeRequest(task_id, task, args)

    task_id = faker.pystr()
    task_name = faker.pystr()
    task_args = [faker.pystr()]

    this_function_raises_exceptions(FakeSelf(task_id, task_name, task_args))

    mail = BaseTests.read_mock_email()
    assert mail.get("body") is not None

    assert f"Celery task {task_id} failed" in mail.get("body")
    assert f"Name: {task_name}" in mail.get("body")
    assert f"Arguments: {str(task_args)}" in mail.get("body")
    assert "Error: Traceback (most recent call last):" in mail.get("body")
    assert 'raise AttributeError("Just an exception")' in mail.get("body")
Пример #28
0
    def do_login(
        cls,
        client: FlaskClient,
        USER: Optional[str],
        PWD: Optional[str],
        status_code: int = 200,
        data: Optional[Dict[str, Any]] = None,
        test_failures: bool = False,
    ) -> Tuple[Optional[Dict[str, str]], str]:
        """
        Make login and return both token and authorization header
        """

        if not Connector.check_availability(
                "authentication"):  # pragma: no cover
            pytest.fail("Authentication is not enabled")

        if USER is None or PWD is None:
            BaseAuthentication.load_default_user()
            BaseAuthentication.load_roles()
        if USER is None:
            USER = BaseAuthentication.default_user
        if PWD is None:
            PWD = BaseAuthentication.default_password

        assert USER is not None
        assert PWD is not None

        if data is None:
            data = {}

        data["username"] = USER
        data["password"] = PWD

        r = client.post(f"{AUTH_URI}/login", json=data)
        content = orjson.loads(r.data.decode("utf-8"))

        if r.status_code == 403:

            # This 403 is expected, return an invalid value or you can enter a loop!
            if status_code == 403:
                return None, content

            if isinstance(content, dict) and content.get("actions"):
                actions = content.get("actions", [])

                for action in actions:
                    if action == "TOTP":
                        continue
                    if action == "FIRST LOGIN":
                        continue
                    if action == "PASSWORD EXPIRED":
                        continue

                data = {}

                if "FIRST LOGIN" in actions or "PASSWORD EXPIRED" in actions:

                    events = cls.get_last_events(1)
                    assert events[0].event == Events.password_expired.value
                    # assert events[0].user == USER

                    newpwd = cls.faker.password(strong=True)
                    if test_failures:
                        data["new_password"] = newpwd
                        data["password_confirm"] = cls.faker.password(
                            strong=True)
                        if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"):
                            data["totp_code"] = BaseTests.generate_totp(USER)

                        BaseTests.do_login(
                            client,
                            USER,
                            PWD,
                            data=data,
                            status_code=409,
                        )

                        # Test failure of password change if TOTP is wrong or missing
                        if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"):
                            data["new_password"] = newpwd
                            data["password_confirm"] = newpwd
                            data.pop("totp_code", None)

                            BaseTests.do_login(
                                client,
                                USER,
                                PWD,
                                data=data,
                                status_code=403,
                            )

                            data["new_password"] = newpwd
                            data["password_confirm"] = newpwd
                            # random int with 6 digits
                            data["totp_code"] = str(
                                cls.faker.pyint(min_value=100000,
                                                max_value=999999))
                            BaseTests.do_login(
                                client,
                                USER,
                                PWD,
                                data=data,
                                status_code=401,
                            )

                    # Change the password to silence FIRST_LOGIN and PASSWORD_EXPIRED
                    data["new_password"] = newpwd
                    data["password_confirm"] = newpwd
                    if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"):
                        data["totp_code"] = BaseTests.generate_totp(USER)
                    BaseTests.do_login(
                        client,
                        USER,
                        PWD,
                        data=data,
                    )
                    # Change again to restore the default password
                    # and keep all other tests fully working
                    data["new_password"] = PWD
                    data["password_confirm"] = PWD
                    if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"):
                        data["totp_code"] = BaseTests.generate_totp(USER)
                    return BaseTests.do_login(
                        client,
                        USER,
                        newpwd,
                        data=data,
                    )

                # in this case FIRST LOGIN has not been executed
                # => login by sending the TOTP code
                if "TOTP" in actions:
                    # Only directly tested => no coverage
                    if test_failures:  # pragma: no cover
                        # random int with 6 digits
                        data["totp_code"] = cls.faker.pyint(min_value=100000,
                                                            max_value=999999)
                        BaseTests.do_login(
                            client,
                            USER,
                            PWD,
                            data=data,
                            status_code=401,
                        )

                    data["totp_code"] = BaseTests.generate_totp(USER)
                    return BaseTests.do_login(
                        client,
                        USER,
                        PWD,
                        data=data,
                    )

        # FOR DEBUGGING WHEN ADVANCED AUTH OPTIONS ARE ON
        # if r.status_code != 200:
        #     c = orjson.loads(r.data.decode("utf-8"))
        #     log.error(c)

        assert r.status_code == status_code

        # when 200 OK content is the token
        assert content is not None

        return {"Authorization": f"Bearer {content}"}, content
Пример #29
0
from restapi import decorators
from restapi.config import get_project_configuration
from restapi.connectors import Connector, smtp
from restapi.endpoints.profile_activation import send_activation_link
from restapi.env import Env
from restapi.exceptions import Conflict, ServiceUnavailable
from restapi.models import Schema, fields, validate
from restapi.rest.definition import EndpointResource, Response
from restapi.services.authentication import DEFAULT_GROUP_NAME
from restapi.utilities.globals import mem

# from restapi.utilities.logs import log

# This endpoint requires the server to send the activation token via email
if Connector.check_availability("smtp"):

    auth = Connector.get_authentication_instance()

    # Note that these are callables returning a model, not models!
    # They will be executed a runtime
    def getInputSchema(request):

        # as defined in Marshmallow.schema.from_dict
        attributes: Dict[str, Union[fields.Field, type]] = {}

        attributes["name"] = fields.Str(required=True)
        attributes["surname"] = fields.Str(required=True)
        attributes["email"] = fields.Email(required=True,
                                           label="Username (email address)")
        attributes["password"] = fields.Str(
Пример #30
0
    def test_websockets(self, client: FlaskClient, faker: Faker) -> None:

        if not Connector.check_availability("pushpin"):
            log.warning("Skipping websockets test: pushpin service not available")
            return

        log.info("Executing websockets tests")

        channel = faker.pystr()
        r = client.post(f"{API_URI}/socket/{channel}")
        assert r.status_code == 401

        r = client.put(f"{API_URI}/socket/{channel}/1")
        assert r.status_code == 401

        headers, _ = self.do_login(client, None, None)
        assert headers is not None
        headers["Content-Type"] = "application/websocket-events"

        r = client.post(f"{API_URI}/socket/{channel}", headers=headers)
        assert r.status_code == 400
        error = "Cannot decode websocket request: invalid in_event"
        assert self.get_content(r) == error

        data = b"\r\n"
        r = client.post(f"{API_URI}/socket/{channel}", data=data, headers=headers)
        assert r.status_code == 400
        error = "Cannot understand websocket request"
        assert self.get_content(r) == error

        data = b"OPEN"
        r = client.post(f"{API_URI}/socket/{channel}", data=data, headers=headers)
        assert r.status_code == 400
        error = "Cannot decode websocket request: invalid format"
        assert self.get_content(r) == error

        data = b"XYZ\r\n"
        r = client.post(f"{API_URI}/socket/{channel}", data=data, headers=headers)
        assert r.status_code == 400
        error = "Cannot understand websocket request"
        assert self.get_content(r) == error

        data = b"OPEN\r\n"
        r = client.post(f"{API_URI}/socket/{channel}", data=data, headers=headers)
        assert r.status_code == 200
        content = r.data.decode("utf-8").split("\n")
        assert len(content) >= 3
        assert content[0] == "OPEN\r"
        assert content[1] == "TEXT 3a\r"
        assert content[2] == 'c:{"channel": "%s", "type": "subscribe"}\r' % channel
        assert "Sec-WebSocket-Extensions" in r.headers
        assert r.headers.get("Sec-WebSocket-Extensions") == "grip"

        r = client.put(f"{API_URI}/socket/{channel}/1", headers=headers)
        assert r.status_code == 200
        assert self.get_content(r) == "Message received: True (sync=True)"

        r = client.put(f"{API_URI}/socket/{channel}/0", headers=headers)
        assert r.status_code == 200
        assert self.get_content(r) == "Message received: True (sync=False)"

        # send message on a different channel
        channel = faker.pystr()
        r = client.put(f"{API_URI}/socket/{channel}/1", headers=headers)
        assert r.status_code == 200
        assert self.get_content(r) == "Message received: True (sync=True)"

        r = client.post(f"{API_URI}/stream/{channel}", headers=headers)
        assert r.status_code == 200
        content = r.data.decode("utf-8")
        assert content == "Stream opened, prepare yourself!\n"
        assert "Grip-Hold" in r.headers
        assert r.headers["Grip-Hold"] == "stream"
        assert "Grip-Channel" in r.headers

        r = client.put(f"{API_URI}/stream/{channel}/1", headers=headers)
        assert r.status_code == 200
        assert self.get_content(r) == "Message received: True (sync=True)"

        r = client.put(f"{API_URI}/stream/{channel}/0", headers=headers)
        assert r.status_code == 200
        assert self.get_content(r) == "Message received: True (sync=False)"