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")
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
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
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")
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
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
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")
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
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
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, ))
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
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")
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
"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"])
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, )