示例#1
0
def sign_in() -> flask.Response:
    if flask.request.method == "GET":
        return flask.make_response(
            flask.render_template("sign-in.html", page_title="Sign in"))
    else:
        crypt_context = flask.current_app.config["CRYPT_CONTEXT"]

        username = flask.request.form["username"]
        cache = get_cache()
        user = user_from_username_if_exists(db.session, cache, username)

        if user is None:
            flask.current_app.logger.info(
                "unsuccessful sign in - no such user: %s", username)
            raise exc.BadRequest("unsuccessful sign in")

        password = flask.request.form["password"]
        if is_correct_password(db.session, crypt_context, user, password):
            flask.current_app.logger.info("successful sign in")

            # In this context the user exists to the api key must too
            api_key = cast(bytes,
                           get_api_key(db.session, get_cache(), user.username))

            set_current_user_for_session(user, api_key)
            flask.flash("Signed in")

            response = flask.make_response("Redirecting...", 303)
            response.headers["Location"] = "/"
            return response
        else:
            flask.current_app.logger.info(
                "unsuccessful sign in - wrong password for user: %s", username)
            raise exc.BadRequest("unsuccessful sign in")
示例#2
0
def user_page_post(username: str) -> Tuple[flask.Response, int]:
    user = user_from_username_if_exists(db.session, get_cache(), username)
    if user is None:
        flask.abort(404, "user not found")
    elif user != get_current_user():
        flask.abort(403, "not allowed to edit the profiles of others")
    else:
        crypt_context = flask.current_app.config["CRYPT_CONTEXT"]

        if flask.request.form.get("old-password") and flask.request.form.get(
                "new-password"):
            old_password = flask.request.form["old-password"]
            if is_correct_password(db.session, crypt_context, user,
                                   old_password):
                new_password = flask.request.form["new-password"]
                set_password(db.session, crypt_context, user, new_password)
            else:
                flask.abort(403, "wrong old password")
        set_user_timezone(db.session, get_cache(), user,
                          flask.request.form["timezone"])
        db.session.commit()
        put_user_in_g()
        flask.flash("Settings updated")
        response = flask.make_response("Redirecting...")
        response.headers["Location"] = flask.url_for("quarchive.user_page",
                                                     username=username)
        return response, 303
示例#3
0
def user_page_post(username: str) -> Tuple[flask.Response, int]:
    user = user_from_username_if_exists(db.session, get_cache(), username)
    if user is None:
        flask.abort(404, "user not found")
    elif user != get_current_user():
        flask.abort(403, "not allowed to edit the profiles of others")
    else:
        set_user_timezone(db.session, get_cache(), user,
                          flask.request.form["timezone"])
        db.session.commit()
        put_user_in_g()
        flask.flash("User updated successfully")
        response = flask.make_response("Redirecting...")
        response.headers["Location"] = flask.url_for("quarchive.user_page",
                                                     username=username)
        return response, 303
示例#4
0
def edit_bookmark(username: str, url_uuid: UUID) -> flask.Response:
    owner = get_user_or_fail(db.session, username)
    require_access_or_fail(
        BookmarkAccessObject(user_uuid=owner.user_uuid, url_uuid=url_uuid),
        Access.WRITE,
    )
    form = flask.request.form
    existing_bookmark = get_bookmark_by_url_uuid(db.session, owner.user_uuid,
                                                 url_uuid)
    if existing_bookmark is None:
        raise exc.NotFound()

    updated_bookmark = Bookmark(
        url=existing_bookmark.url,
        created=existing_bookmark.created,
        title=form["title"],
        description=form["description"],
        unread="unread" in form,
        deleted="deleted" in form,
        updated=datetime.utcnow().replace(tzinfo=timezone.utc),
        tag_triples=tag_triples_from_form(
            form, current=existing_bookmark.tag_triples),
    )

    merged_bookmark = updated_bookmark.merge(existing_bookmark)

    set_bookmark(db.session, get_cache(), owner.user_uuid, merged_bookmark)
    db.session.commit()
    flask.flash("Edited: %s" % merged_bookmark.title)
    return flask.make_response("ok")
示例#5
0
def test_caching_user(test_user):
    cache = get_cache()
    key = UserUUIDToUserKey(test_user.user_uuid)

    cache.set(key, test_user)

    assert cache.get(key) == test_user
示例#6
0
def get_user_or_fail(session: Session, username: str) -> User:
    """Get the user for that username, or raise a 404 if it does not exist."""
    user = user_from_username_if_exists(db.session, get_cache(), username)
    if user is None:
        log.warn("user '%s' does not exist", username)
        flask.abort(404, "no such user")
    else:
        return user
示例#7
0
def share_grant_to_url(session: Session, share_grant: ShareGrant) -> str:
    access_obj = share_grant.access_object
    if isinstance(access_obj, BookmarkAccessObject):
        user = user_from_user_uuid(session, get_cache(), access_obj.user_uuid)
        # FIXME: Can this really happen?
        if user is None:
            raise RuntimeError("user not found")
        return flask.url_for(
            "quarchive.view_bookmark",
            username=user.username,
            url_uuid=access_obj.url_uuid,
        )
    else:
        raise NotImplementedError(
            "no implementation for other kinds of access obj")
示例#8
0
def create_bookmark(username: str) -> flask.Response:
    owner = get_user_or_fail(db.session, username)
    # FIXME: sort out optional url_uuid
    require_access_or_fail(
        UserBookmarksAccessObject(user_uuid=owner.user_uuid),
        Access.WRITE,
    )
    form = flask.request.form
    creation_time = datetime.utcnow().replace(tzinfo=timezone.utc)
    tag_triples = tag_triples_from_form(form)

    url_str = form["url"]
    try:
        # As it's a user entering this url, help them along with getting a
        # sufficiently canonicalised url
        url = URL.from_string(url_str, coerce_canonicalisation=True)
    except DisallowedSchemeException:
        log.warning("user tried to create url: %s (disallowed scheme)",
                    url_str)
        flask.abort(400, "invalid url (disallowed scheme)")

    bookmark = Bookmark(
        url=url,
        title=form["title"],
        description=form["description"],
        unread="unread" in form,
        deleted=False,
        updated=creation_time,
        created=creation_time,
        tag_triples=tag_triples,
    )
    url_uuid = set_bookmark(db.session, get_cache(), owner.user_uuid, bookmark)
    db.session.commit()
    publish_message(
        message_lib.BookmarkCreated(user_uuid=owner.user_uuid,
                                    url_uuid=url.url_uuid),
        environ["QM_RABBITMQ_BG_WORKER_TOPIC"],
    )
    flask.flash("Bookmarked: %s" % bookmark.title)
    response = flask.make_response("Redirecting...", 303)
    response.headers["Location"] = flask.url_for(
        "quarchive.edit_bookmark_form",
        url_uuid=url_uuid,
        username=owner.username,
    )
    return response
示例#9
0
    def wrapper(*args, **kwargs):
        try:
            # FIXME: Remove old synonyms
            username = (flask.request.headers.get("X-QM-API-Username")
                        or flask.request.headers["Quarchive-Username"])
            api_key_str = (flask.request.headers.get("X-QM-API-Key")
                           or flask.request.headers["Quarchive-API-Key"])
        except KeyError:
            flask.current_app.logger.info("no api credentials")
            return flask.jsonify({"error": "no api credentials"}), 400

        cache = get_cache()

        try:
            api_key = bytes.fromhex(api_key_str)
        except ValueError:
            flask.current_app.logger.warning("invalid api key: %s", username)
            return (
                flask.jsonify(
                    {"error": "invalid api key (should be hexadecimal)"}),
                400,
            )

        if is_correct_api_key(db.session, cache, username, api_key):
            # We know at this point that the user does in fact exist, so cast
            # away the Optional
            user = cast(
                User, user_from_username_if_exists(db.session, cache,
                                                   username))
            set_current_user(user)
            return handler(user)
        else:
            # Something was wrong, let's figure out what
            user_if_exists = user_from_username_if_exists(
                db.session, cache, username)
            if user_if_exists is None:
                flask.current_app.logger.warning("user does not exist: %s",
                                                 username)
                # user doesn't exist
                return flask.jsonify({"error": "user does not exist"}), 400
            else:
                flask.current_app.logger.warning("wrong api key for %s",
                                                 username)
                # api key must have been wrong
                return flask.jsonify({"error": "wrong api key"}), 400
示例#10
0
def user_page(username: str) -> flask.Response:
    cache = get_cache()
    user = user_from_username_if_exists(db.session, cache, username)
    api_key: Optional[bytes]
    if user is None:
        flask.abort(404, "user not found")
    elif user == get_current_user():
        api_key = get_api_key(db.session, cache, user.username)
    else:
        api_key = None
    return flask.make_response(
        flask.render_template(
            "user.html",
            user=user,
            api_key=api_key,
            timezones=pytz.common_timezones,
            current_timezone=user.timezone.zone,
        ))
示例#11
0
def quick_add_tag(username: str, url_uuid: UUID) -> flask.Response:
    owner = get_user_or_fail(db.session, username)
    require_access_or_fail(
        BookmarkAccessObject(user_uuid=owner.user_uuid, url_uuid=url_uuid),
        Access.WRITE)
    bookmark = get_bookmark_by_url_uuid_or_fail(db.session, owner.user_uuid,
                                                url_uuid)
    tag = flask.request.form["tag"]
    set_bookmark(db.session, get_cache(), owner.user_uuid,
                 bookmark.with_tag(tag))
    db.session.commit()
    flask.flash(f"Tagged '{bookmark.title}' with '{tag}'")
    response = flask.make_response("Redirecting...", 303)
    response.headers["Location"] = flask.url_for(
        "quarchive.edit_bookmark_form",
        url_uuid=url_uuid,
        username=owner.username,
    )
    return response
示例#12
0
def put_user_in_g() -> None:
    app_logger = flask.current_app.logger
    user_uuid: Optional[Any] = flask.session.get("user_uuid")
    if user_uuid is not None:
        if not isinstance(user_uuid, UUID):
            del flask.session["user_uuid"]
            app_logger.warning("cleared a corrupt user_uuid cookie: %s",
                               user_uuid)
        else:
            user = user_from_user_uuid(db.session, get_cache(), user_uuid)
            if user is None:
                del flask.session["user_uuid"]
                app_logger.warning("cleared a corrupt user_uuid cookie: %s",
                                   user_uuid)
            else:
                set_current_user(user)
                app_logger.debug("currently signed in as: %s",
                                 get_current_user())
    else:
        app_logger.debug("not signed in")
示例#13
0
def register() -> flask.Response:
    if flask.request.method == "GET":
        return flask.make_response(
            flask.render_template("register.html", page_title="Register"))
    else:
        username = flask.request.form["username"]
        password_plain = flask.request.form["password"]
        email: Optional[str] = flask.request.form["email"] or None
        cache = get_cache()

        if username_exists(db.session, username):
            log.error("username already registered: %s", username)
            flask.abort(400, description="username already exists")

        username_regex = r"^[A-z0-9_\-]+$"
        if not re.compile(username_regex).match(username):
            log.error("invalid username: %s", username)
            flask.abort(400,
                        description="invalid username - must match %s" %
                        username_regex)

        user, api_key = create_user(
            db.session,
            cache,
            flask.current_app.config["CRYPT_CONTEXT"],
            username,
            password_plain,
            email,
        )

        db.session.commit()
        response = flask.make_response("Redirecting...", 303)
        response.headers["Location"] = flask.url_for("quarchive.my_bookmarks")
        log.info("created user: %s", user.username)

        set_current_user_for_session(user, api_key)

        return response
示例#14
0
            "QM_RABBITMQ_BG_WORKER_TOPIC":
            "bg_q",
            "QM_MISSIVE_SQLITE_DLQ_CONNSTRING":
            "file:quarchive_test?mode=memory&cache=shared",
        },
    ):
        yield


@pytest.fixture(scope="function")
def session(app, config):
    return sut.db.session


caches = [
    pytest.param(get_cache(), id="memcache"),
    pytest.param(NullCache(), id="nullcache"),
]


@pytest.fixture(params=caches)
def cache(app, config, request):
    return request.param


@pytest.fixture(scope="session")
def app(config):
    a = sut.init_app()
    a.config["TESTING"] = True
    # Speeds things up considerably when testing
    a.config["CRYPT_CONTEXT"] = CryptContext(["plaintext"])
示例#15
0
def sync(current_user: User) -> Tuple[flask.Response, int]:
    start_time = datetime.utcnow()
    extension_version = flask.request.headers.get(
        "Quarchive-Extension-Version", "unknown")
    log.debug("extension version: %s", extension_version)
    user_uuid = current_user.user_uuid
    use_jsonlines = flask.request.headers["Content-Type"] != "application/json"
    if not use_jsonlines:
        log.warning("sync request using deprecated single json object")
        body = flask.request.json
        recieved_bookmarks = (Bookmark.from_json(item)
                              for item in body["bookmarks"])
    else:
        log.info("sync request using jsonlines")
        recieved_bookmarks = (Bookmark.from_json(json.loads(l))
                              for l in flask.request.stream.readlines())

    try:
        merge_result = merge_bookmarks(db.session, get_cache(), user_uuid,
                                       recieved_bookmarks)
    except BadCanonicalisationException as e:
        log.error(
            "bad canonicalised url ('%s') from version %s, user %s",
            e.url_string,
            extension_version,
            current_user,
        )
        db.session.rollback()
        flask.abort(400, "bad canonicalisation on url: %s" % e.url_string)
    db.session.commit()

    for added in merge_result.added:
        publish_message(
            message_lib.BookmarkCreated(user_uuid=user_uuid,
                                        url_uuid=added.url.url_uuid),
            environ["QM_RABBITMQ_BG_WORKER_TOPIC"],
        )

    is_full_sync = "full" in flask.request.args

    if is_full_sync:
        response_bookmarks = all_bookmarks(db.session, current_user.user_uuid)
    else:
        response_bookmarks = merge_result.changed

    # If we got JSON, send json back
    if not use_jsonlines:
        return flask.json.jsonify(
            {"bookmarks": [b.to_json() for b in response_bookmarks]})
    else:

        def generator():
            for b in response_bookmarks:
                yield json.dumps(b.to_json())
                yield "\n"
            if is_full_sync:
                duration = datetime.utcnow() - start_time
                log.info(
                    "completed full sync for %s in %ds",
                    current_user.username,
                    duration.total_seconds(),
                )

        return (
            flask.Response(
                flask.stream_with_context(generator()),
                mimetype="application/x-ndjson",
            ),
            200,
        )