Example #1
0
def user_registration_input(request: FlaskRequest) -> Type[Schema]:

    attributes: MarshmallowSchema = {}

    attributes["name"] = fields.Str(required=True)
    attributes["surname"] = fields.Str(required=True)
    # This is because Email is not typed on marshmallow
    attributes["email"] = fields.Email(  # type: ignore
        required=True,
        metadata={"label": "Username (email address)"},
        validate=validate.Length(max=100),
    )
    attributes["password"] = fields.Str(
        required=True,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
        metadata={"password": True},
    )
    attributes["password_confirm"] = fields.Str(
        required=True,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
        metadata={
            "label": "Password confirmation",
            "password": True
        },
    )

    if custom_fields := mem.customizer.get_custom_input_fields(
            request=None, scope=mem.customizer.REGISTRATION):
        attributes.update(custom_fields)
Example #2
0
class NewPassword(Schema):
    password = fields.Str(
        required=True,
        metadata={"password": True},
        # Not needed to check the length of the current password... if set...
        # validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
    )
    new_password = fields.Str(
        required=True,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
        metadata={"password": True},
    )
    password_confirm = fields.Str(
        required=True,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
        metadata={"password": True},
    )
    totp_code = fields.TOTP(required=False)
Example #3
0
class Credentials(Schema):
    username = fields.Email(required=True)
    password = fields.Str(
        required=True,
        password=True,
        # Otherwise default testing password, like test, will fail
        # validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH)
    )
    new_password = fields.Str(
        required=False,
        password=True,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
    )
    password_confirm = fields.Str(
        required=False,
        password=True,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
    )
    totp_code = TOTP(required=False)
Example #4
0
class Credentials(Schema):
    # This is because Email is not typed on marshmallow
    username = fields.Email(  # type: ignore
        required=True, validate=validate.Length(max=100))
    password = fields.Str(
        required=True,
        metadata={"password": True},
        # Otherwise default testing password, like test, will fail
        # validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH)
    )
    new_password = fields.Str(
        required=False,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
        metadata={"password": True},
    )
    password_confirm = fields.Str(
        required=False,
        validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
        metadata={"password": True},
    )
    totp_code = fields.TOTP(required=False)
Example #5
0
    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(
            required=True,
            password=True,
            validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
        )
        attributes["password_confirm"] = fields.Str(
            required=True,
            password=True,
            label="Password confirmation",
            validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
        )

        if custom_fields := mem.customizer.get_custom_input_fields(
                request=None, scope=mem.customizer.REGISTRATION):
            attributes.update(custom_fields)
Example #6
0
 class InputSchema(Schema):
     mystr = fields.Str(required=True, validate=validate.Length(min=1))
     mydate = fields.DateTime(
         required=True,
         format=ISO8601UTC,
         # validate=validate.Range(
         #     max=datetime.now(pytz.utc).replace(hour=23, minute=59, second=59),
         #     min=datetime(1900, 1, 1, tzinfo=pytz.utc),
         #     max_inclusive=True,
         #     error="Invalid date",
         # ),
     )
     myint_exclusive = fields.Int(
         required=True,
         validate=validate.Range(
             min=1, max=10, min_inclusive=False, max_inclusive=False
         ),
     )
     myint_inclusive = fields.Int(
         required=True,
         validate=validate.Range(min=1, max=10),
     )
Example #7
0
class MailInput(Schema):
    subject = fields.Str(required=True,
                         metadata={"description": "Subject of your email"})
    body = fields.Str(
        required=True,
        validate=validate.Length(max=9999),
        metadata={
            "description": "Body of your email. You can use html code here."
        },
    )
    # This is because Email is not typed on marshmallow
    to = fields.Email(  # type: ignore
        required=True,
        metadata={"label": "Destination email address"})
    cc = fields.DelimitedList(
        # This is because Email is not typed on marshmallow
        fields.Email(),  # type: ignore
        metadata={
            "label": "CC - Carbon Copy",
            "description": "CC email addresses (comma-delimited list)",
        },
    )
    bcc = fields.DelimitedList(
        # This is because Email is not typed on marshmallow
        fields.Email(),  # type: ignore
        metadata={
            "label": "BCC - Blind Carbon Copy",
            "description": "BCC email addresses (comma-delimited list)",
        },
    )
    dry_run = fields.Boolean(
        required=True,
        metadata={
            "label": "Dry run execution",
            "description": "Only simulate the email, do not send it",
        },
    )
Example #8
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)
Example #9
0
    class RecoverPassword(EndpointResource):

        depends_on = [
            "MAIN_LOGIN_ENABLE", "ALLOW_PASSWORD_RESET", "AUTH_ENABLE"
        ]
        labels = ["authentication"]

        @decorators.use_kwargs(
            # This is because Email is not typed on marshmallow
            {"reset_email": fields.Email(required=True)}  # type: ignore
        )
        @decorators.endpoint(
            path="/auth/reset",
            summary="Request password reset via email",
            description="Request password reset via email",
            responses={
                200: "Reset email is valid",
                400: "Invalid reset email",
                403: "Account not found or already active",
            },
        )
        def post(self, reset_email: str) -> Response:

            reset_email = reset_email.lower()

            self.auth.verify_blocked_username(reset_email)

            user = self.auth.get_user(username=reset_email)

            if user is None:
                raise Forbidden(
                    f"Sorry, {reset_email} is not recognized as a valid username",
                )

            self.auth.verify_user_status(user)

            reset_token, payload = self.auth.create_temporary_token(
                user, self.auth.PWD_RESET)

            server_url = get_frontend_url()

            rt = reset_token.replace(".", "+")

            uri = Env.get("RESET_PASSWORD_URI", "/public/reset")
            complete_uri = f"{server_url}{uri}/{rt}"

            sent = send_password_reset_link(user, complete_uri, reset_email)

            if not sent:  # pragma: no cover
                raise ServiceUnavailable("Error sending email, please retry")

            ##################
            # Completing the reset task
            self.auth.save_token(user,
                                 reset_token,
                                 payload,
                                 token_type=self.auth.PWD_RESET)

            msg = "We'll send instructions to the email provided if it's associated "
            msg += "with an account. Please check your spam/junk folder."

            self.log_event(self.events.reset_password_request, user=user)
            return self.response(msg)

        @decorators.use_kwargs({
            "new_password":
            fields.Str(
                required=False,
                validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
                metadata={"password": True},
            ),
            "password_confirm":
            fields.Str(
                required=False,
                validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH),
                metadata={"password": True},
            ),
        })
        @decorators.endpoint(
            path="/auth/reset/<token>",
            summary="Change password as conseguence of a reset request",
            description="Change password as conseguence of a reset request",
            responses={
                200: "Reset token is valid, password changed",
                400: "Invalid reset token",
            },
        )
        def put(
            self,
            token: str,
            new_password: Optional[str] = None,
            password_confirm: Optional[str] = None,
        ) -> Response:

            token = token.replace("%2B", ".")
            token = token.replace("+", ".")

            try:
                # valid, token, jti, user
                _, _, jti, user = self.auth.verify_token(
                    token, raiseErrors=True, token_type=self.auth.PWD_RESET)

            # If token is expired
            except jwt.exceptions.ExpiredSignatureError:
                raise BadRequest(
                    "Invalid reset token: this request is expired")

            # if token is not active yet
            except jwt.exceptions.ImmatureSignatureError as e:
                log.info(e)
                raise BadRequest("Invalid reset token")
            # if token does not exist (or other generic errors)
            except Exception as e:
                log.info(e)
                raise BadRequest("Invalid reset token")

            if user is None:  # pragma: no cover
                raise BadRequest("Invalid activation token")

            # Recovering token object from jti
            tokens_obj = self.auth.get_tokens(token_jti=jti)
            # Can't happen because the token is refused from verify_token function
            if len(tokens_obj) == 0:  # pragma: no cover
                raise BadRequest(
                    "Invalid reset token: this request is no longer valid")

            token_obj = tokens_obj.pop(0)
            emitted = token_obj["emitted"]

            last_change = None
            # If user logged in after the token emission invalidate the token
            if user.last_login is not None:
                last_change = user.last_login
            # If user changed the pwd after the token emission invalidate the token
            # Can't happen because the change password also invalidated the token
            elif user.last_password_change is not None:  # pragma: no cover
                last_change = user.last_password_change

            if last_change is not None:

                # Can't happen because the change password also invalidated the token
                if last_change > emitted:  # pragma: no cover
                    self.auth.invalidate_token(token)
                    raise BadRequest(
                        "Invalid reset token: this request is no longer valid",
                    )

            # The reset token is valid, do something

            # No password to be changed, just a token verification
            if new_password is None and password_confirm is None:
                return self.empty_response()

            # Something is missing
            if new_password is None or password_confirm is None:
                raise BadRequest("Invalid password")

            if new_password != password_confirm:
                raise BadRequest(
                    "New password does not match with confirmation")

            self.auth.change_password(user, user.password, new_password,
                                      password_confirm)
            # I really don't know why this save is required... since it is already
            # in change_password ... But if I remove it the new pwd is not saved...
            self.auth.save_user(user)

            # Bye bye token (reset tokens are valid only once)
            self.auth.invalidate_token(token)

            return self.response("Password changed")
Example #10
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)
Example #11
0
    class InputSchema(Schema):
        # lowercase key without label defined. label will be key.title() in schema
        mystr = fields.Str(required=True, validate=validate.Length(min=4))
        # non-lowercase key without label defined. label will be == to key in schema
        MYDATE = fields.Date(required=True)
        MYDATETIME = fields.AwareDateTime(
            required=True,
            format=ISO8601UTC,
            default_timezone=pytz.utc,
            validate=validate.Range(
                max=datetime.now(pytz.utc).replace(hour=23, minute=59, second=59),
                min=datetime(1900, 1, 1, tzinfo=pytz.utc),
                max_inclusive=True,
                error="Invalid date",
            ),
        )
        myint_exclusive = fields.Int(
            required=True,
            # Explicit label definition... but missing description
            validate=validate.Range(
                min=1, max=10, min_inclusive=False, max_inclusive=False
            ),
            metadata={
                "label": "Int exclusive field",
            },
        )
        myint_inclusive = fields.Int(
            required=True,
            # Both label and description explicit definition
            validate=validate.Range(min=1, max=10),
            metadata={
                "label": "Int inclusive field",
                "description": "This field accepts values in a defined range",
            },
        )

        myselect = fields.Str(
            required=True,
            validate=validate.OneOf(choices=["a", "b"], labels=["A", "B"]),
        )

        myselect2 = fields.Str(
            required=True,
            # Wrong definition, number labels < number of choices
            # Labels will be ignored and replaced by choices
            validate=validate.OneOf(choices=["a", "b"], labels=["A"]),
        )

        mymaxstr = fields.Str(required=True, validate=validate.Length(max=7))

        myequalstr = fields.Str(required=True, validate=validate.Length(equal=6))

        # Note: requests (from pytest) has to json-dump the arrays and objects,
        # but the normal Marshmallow fields does not json-load the inputs

        # fields.Nested is a replacement of the default Nested field with the ability
        # to receive json dumped data from requests or pytest
        mynested = fields.Nested(Nested, required=True)

        mynullablenested = fields.Nested(Nested, required=True, allow_none=True)

        # fields.List is a replacement of the default List field with the ability
        # to receive json dumped data from requests or pytest

        # In json model the type of this field will be resolved as string[]
        mylist = fields.List(fields.Str(), required=True)
        # In json model the type of this field will be resolved as int[]
        mylist2 = fields.List(CustomInt, required=True)
        # In json model the type of this field will be resolved as mylist3[]
        # The type is key[] ... should be something more explicative like FieldName[]
        mylist3 = fields.List(CustomGenericField, required=True)