async def authenticate(self, conn: HTTPConnection): unauthenticated = (None, AnonymousUser()) sid = conn.cookies.get("AURSID") if not sid: return unauthenticated timeout = aurweb.config.getint("options", "login_timeout") remembered = ("AURREMEMBER" in conn.cookies and bool(conn.cookies.get("AURREMEMBER"))) if remembered: timeout = aurweb.config.getint("options", "persistent_cookie_timeout") # If no session with sid and a LastUpdateTS now or later exists. now_ts = time.utcnow() record = db.query(Session).filter(Session.SessionID == sid).first() if not record: return unauthenticated elif record.LastUpdateTS < (now_ts - timeout): with db.begin(): db.delete_all([record]) return unauthenticated # At this point, we cannot have an invalid user if the record # exists, due to ForeignKey constraints in the schema upheld # by mysqlclient. with db.begin(): user = db.query(User).filter(User.ID == record.UsersID).first() user.nonce = util.make_nonce() user.authenticated = True return (AuthCredentials(["authenticated"]), user)
def test_usermaint(user: User): """ In this case, we first test that only the expired record gets updated, but the non-expired record remains untouched. After, we update the login time on the non-expired record and exercise its code path. """ now = time.utcnow() limit_to = now - 86400 * 7 with db.begin(): user.LastLoginIPAddress = "127.0.0.1" user.LastLogin = limit_to - 666 user.LastSSHLoginIPAddress = "127.0.0.1" user.LastSSHLogin = now - 10 usermaint.main() assert user.LastLoginIPAddress is None assert user.LastSSHLoginIPAddress == "127.0.0.1" with db.begin(): user.LastSSHLogin = limit_to - 666 usermaint.main() assert user.LastLoginIPAddress is None assert user.LastSSHLoginIPAddress is None
async def request_close_post(request: Request, id: int, comments: str = Form(default=str())): pkgreq = get_pkgreq_by_id(id) # `pkgreq`.User can close their own request. approved = [pkgreq.User] if not request.user.has_credential(creds.PKGREQ_CLOSE, approved=approved): # Request user doesn't have permission here: redirect to '/'. return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) context = make_context(request, "Close Request") context["pkgreq"] = pkgreq now = time.utcnow() with db.begin(): pkgreq.Closer = request.user pkgreq.ClosureComment = comments pkgreq.ClosedTS = now pkgreq.Status = REJECTED_ID notify_ = notify.RequestCloseNotification(request.user.ID, pkgreq.ID, pkgreq.status_display()) notify_.send() return RedirectResponse("/requests", status_code=HTTPStatus.SEE_OTHER)
def test_homepage_dashboard_flagged(user: User, user2: User, package: Package): pkgbase = package.PackageBase now = time.utcnow() with db.begin(): db.create(PackageComaintainer, User=user2, PackageBase=pkgbase, Priority=1) pkgbase.OutOfDateTS = now - 5 pkgbase.Flagger = user # Test that a comaintainer viewing the dashboard shows them their # flagged co-maintained packages. comaint_cookies = {"AURSID": user2.login(Request(), "testPassword")} with client as request: resp = request.get("/", cookies=comaint_cookies) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) flagged = root.xpath('//table[@id="flagged-packages"]//tr/td/a')[0] assert flagged.text.strip() == package.Name # Test that a maintainer viewing the dashboard shows them their # flagged maintained packages. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: resp = request.get("/", cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) flagged = root.xpath('//table[@id="flagged-packages"]//tr/td/a')[0] assert flagged.text.strip() == package.Name
def make_context(request: Request, title: str, next: str = None): """ Create a context for a jinja2 TemplateResponse. """ import aurweb.auth.creds commit_url = aurweb.config.get_with_fallback("devel", "commit_url", None) commit_hash = aurweb.config.get_with_fallback("devel", "commit_hash", None) if commit_hash: # Shorten commit_hash to a short Git hash. commit_hash = commit_hash[:7] timezone = time.get_request_timezone(request) language = l10n.get_request_language(request) return { "request": request, "commit_url": commit_url, "commit_hash": commit_hash, "language": language, "languages": l10n.SUPPORTED_LANGUAGES, "rtl": language in l10n.RIGHT_TO_LEFT_LANGUAGES, "timezone": timezone, "timezones": time.SUPPORTED_TIMEZONES, "title": title, "now": time.now(timezone), "utcnow": time.utcnow(), "config": aurweb.config, "creds": aurweb.auth.creds, "next": next if next else request.url.path, "version": os.environ.get("COMMIT_HASH", aurweb.config.AURWEB_VERSION) }
async def pkgbase_comment_pin(request: Request, name: str, id: int, next: str = Form(default=None)): """ Pin a comment. :param request: FastAPI Request :param name: PackageBase.Name :param id: PackageComment.ID :param next: Optional `next` parameter used in the POST request :return: RedirectResponse to `next` """ pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) has_cred = request.user.has_credential(creds.COMMENT_PIN, approved=comment.maintainers()) if not has_cred: _ = l10n.get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, detail=_("You are not allowed to pin this comment.")) now = time.utcnow() with db.begin(): comment.PinnedTS = now if not next: next = f"/pkgbase/{name}" return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER)
def test_request_post_orphan_autogenerated_closure(client: TestClient, tu_user: User, pkgbase: PackageBase, pkgreq: PackageRequest): idle_time = config.getint("options", "request_idle_time") now = time.utcnow() with db.begin(): pkgreq.ReqTypeID = ORPHAN_ID # Set the request time so it's seen as due (idle_time has passed). pkgreq.RequestTS = now - idle_time - 10 endpoint = f"/pkgbase/{pkgbase.Name}/disown" data = {"confirm": True} with client as request: resp = request.post(endpoint, data=data, cookies=tu_user.cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" assert Email.count() == 1 email = Email(1) expr = r"^\[PRQ#\d+\] Orphan Request for .+ Accepted$" assert re.match(expr, email.headers.get("Subject")) expr = r"\[Autogenerated\] Accepted orphan for .+\." assert re.search(expr, email.body)
async def pkgbase_comments_post( request: Request, name: str, comment: str = Form(default=str()), enable_notifications: bool = Form(default=False)): """ Add a new comment via POST request. """ pkgbase = get_pkg_or_base(name, PackageBase) if not comment: raise HTTPException(status_code=HTTPStatus.BAD_REQUEST) # If the provided comment is different than the record's version, # update the db record. now = time.utcnow() with db.begin(): comment = db.create(PackageComment, User=request.user, PackageBase=pkgbase, Comments=comment, RenderedComment=str(), CommentTS=now) if enable_notifications and not request.user.notified(pkgbase): db.create(PackageNotification, User=request.user, PackageBase=pkgbase) update_comment_render_fastapi(comment) notif = notify.CommentNotification(request.user.ID, pkgbase.ID, comment.ID) notif.send() # Redirect to the pkgbase page. return RedirectResponse(f"/pkgbase/{pkgbase.Name}#comment-{comment.ID}", status_code=HTTPStatus.SEE_OTHER)
async def pkgbase_flag_post(request: Request, name: str, comments: str = Form(default=str())): pkgbase = get_pkg_or_base(name, PackageBase) if not comments: context = templates.make_context(request, "Flag Package Out-Of-Date") context["pkgbase"] = pkgbase context["errors"] = [ "The selected packages have not been flagged, " "please enter a comment." ] return render_template(request, "pkgbase/flag.html", context, status_code=HTTPStatus.BAD_REQUEST) has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if has_cred and not pkgbase.OutOfDateTS: now = time.utcnow() with db.begin(): pkgbase.OutOfDateTS = now pkgbase.Flagger = request.user pkgbase.FlaggerComment = comments notify.FlagNotification(request.user.ID, pkgbase.ID).send() return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
def simple(U: str = str(), E: str = str(), H: bool = False, BE: str = str(), R: str = str(), HP: str = str(), I: str = str(), K: str = str(), J: bool = False, CN: bool = False, UN: bool = False, ON: bool = False, S: bool = False, user: models.User = None, **kwargs) -> None: now = time.utcnow() with db.begin(): user.Username = U or user.Username user.Email = E or user.Email user.HideEmail = strtobool(H) user.BackupEmail = user.BackupEmail if BE is None else BE user.RealName = user.RealName if R is None else R user.Homepage = user.Homepage if HP is None else HP user.IRCNick = user.IRCNick if I is None else I user.PGPKey = user.PGPKey if K is None else K user.Suspended = strtobool(S) user.InactivityTS = now * int(strtobool(J)) user.CommentNotify = strtobool(CN) user.UpdateNotify = strtobool(UN) user.OwnershipNotify = strtobool(ON)
def _main(): # One day behind. limit_to = time.utcnow() - 86400 query = db.query(PackageBase).filter( and_(PackageBase.SubmittedTS < limit_to, PackageBase.PackagerUID.is_(None))) db.delete_all(query)
def pkgbase(user: User) -> PackageBase: now = time.utcnow() with db.begin(): pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=user, SubmittedTS=now, ModifiedTS=now) yield pkgbase
def pkgbase(user: User) -> PackageBase: now = time.utcnow() with db.begin(): pkgbase = db.create(PackageBase, Packager=user, Name="pkgbase_0", SubmittedTS=now, ModifiedTS=now) yield pkgbase
def test_generate_unique_sid_exhausted(client: TestClient, user: User, caplog: pytest.LogCaptureFixture): """ In this test, we mock up generate_unique_sid() to infinitely return the same SessionID given to `user`. Within that mocking, we try to login as `user2` and expect the internal server error rendering by our error handler. This exercises the bad path of /login, where we can't find a unique SID to assign the user. """ now = time.utcnow() with db.begin(): # Create a second user; we'll login with this one. user2 = db.create(User, Username="******", Email="*****@*****.**", ResetKey="testReset", Passwd="testPassword", AccountTypeID=USER_ID) # Create a session with ID == "testSession" for `user`. db.create(Session, User=user, SessionID="testSession", LastUpdateTS=now) # Mock out generate_unique_sid; always return "testSession" which # causes us to eventually error out and raise an internal error. def mock_generate_sid(): return "testSession" # Login as `user2`; we expect an internal server error response # with a relevent detail. post_data = { "user": user2.Username, "passwd": "testPassword", "next": "/", } generate_unique_sid_ = "aurweb.models.session.generate_unique_sid" with mock.patch(generate_unique_sid_, mock_generate_sid): with client as request: # Set cookies = {} to remove any previous login kept by TestClient. response = request.post("/login", data=post_data, cookies={}) assert response.status_code == int(HTTPStatus.INTERNAL_SERVER_ERROR) assert "500 - Internal Server Error" in response.text # Make sure an IntegrityError from the DB got logged out # with a FATAL traceback ID. expr = r"FATAL\[.{7}\]" assert re.search(expr, caplog.text) assert "IntegrityError" in caplog.text expr = r"Duplicate entry .+ for key .+SessionID.+" assert re.search(expr, response.text)
def _main(): limit_to = time.utcnow() - 86400 * 7 update_ = update(User).where(User.LastLogin < limit_to).values( LastLoginIPAddress=None) db.get_session().execute(update_) update_ = update(User).where(User.LastSSHLogin < limit_to).values( LastSSHLoginIPAddress=None) db.get_session().execute(update_)
def tu_voteinfo(user: User) -> TUVoteInfo: ts = time.utcnow() with db.begin(): tu_voteinfo = db.create(TUVoteInfo, Agenda="Blah blah.", User=user.Username, Submitted=ts, End=ts + 5, Quorum=0.5, Submitter=user) yield tu_voteinfo
def test_query_voted(maintainer: User, package: Package): now = time.utcnow() with db.begin(): db.create(PackageVote, User=maintainer, VoteTS=now, PackageBase=package.PackageBase) query = db.query(Package).filter(Package.ID == package.ID).all() query_voted = util.query_voted(query, maintainer) assert query_voted[package.PackageBase.ID]
def voteinfo(user: User) -> TUVoteInfo: now = time.utcnow() start = config.getint("tuvotereminder", "range_start") with db.begin(): voteinfo = db.create(TUVoteInfo, Agenda="Lorem ipsum.", User=user.Username, End=(now + start + 1), Quorum=0.00, Submitter=user, Submitted=0) yield voteinfo
def test_package_vote_creation(user: User, pkgbase: PackageBase): ts = time.utcnow() with db.begin(): package_vote = db.create(PackageVote, User=user, PackageBase=pkgbase, VoteTS=ts) assert bool(package_vote) assert package_vote.User == user assert package_vote.PackageBase == pkgbase assert package_vote.VoteTS == ts
def packages(user): pkgs = [] now = time.utcnow() # Create 101 packages; we limit 100 on RSS feeds. with db.begin(): for i in range(101): pkgbase = db.create( PackageBase, Maintainer=user, Name=f"test-package-{i}", SubmittedTS=(now + i), ModifiedTS=(now + i)) pkg = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) pkgs.append(pkg) yield pkgs
def verify_orphan_request(user: User, pkgbase: PackageBase): """ Verify that an undue orphan request exists in `requests`. """ requests = pkgbase.requests.filter(PackageRequest.ReqTypeID == ORPHAN_ID) for pkgreq in requests: idle_time = config.getint("options", "request_idle_time") time_delta = time.utcnow() - pkgreq.RequestTS is_due = pkgreq.Status == PENDING_ID and time_delta > idle_time if is_due: # If the requester is the pkgbase maintainer or the # request is already due, we're good to go: return True. return True return False
def test_session_cs(): """ Test case sensitivity of the database table. """ with db.begin(): user2 = db.create(User, Username="******", Email="*****@*****.**", ResetKey="testReset2", Passwd="testPassword", AccountTypeID=USER_ID) with db.begin(): session_cs = db.create(Session, User=user2, SessionID="TESTSESSION", LastUpdateTS=time.utcnow()) assert session_cs.SessionID == "TESTSESSION" assert session_cs.SessionID != "testSession"
def login(self, request: Request, password: str, session_time: int = 0) -> str: """ Login and authenticate a request. """ from aurweb import db from aurweb.models.session import Session, generate_unique_sid if not self._login_approved(request): return None self.authenticated = self.valid_password(password) if not self.authenticated: return None # Maximum number of iterations where we attempt to generate # a unique SID. In cases where the Session table has # exhausted all possible values, this will catch exceptions # instead of raising them and include details about failing # generation in an HTTPException. tries = 36 exc = None for i in range(tries): exc = None now_ts = time.utcnow() try: with db.begin(): self.LastLogin = now_ts self.LastLoginIPAddress = request.client.host if not self.session: sid = generate_unique_sid() self.session = db.create(Session, User=self, SessionID=sid, LastUpdateTS=now_ts) else: last_updated = self.session.LastUpdateTS if last_updated and last_updated < now_ts: self.session.SessionID = generate_unique_sid() self.session.LastUpdateTS = now_ts # Unset InactivityTS, we've logged in! self.InactivityTS = 0 break except IntegrityError as exc_: exc = exc_ if exc: raise exc return self.session.SessionID
def test_usermaint_noop(user: User): """ Last[SSH]Login isn't expired in this test: usermaint is noop. """ now = time.utcnow() with db.begin(): user.LastLoginIPAddress = "127.0.0.1" user.LastLogin = now - 10 user.LastSSHLoginIPAddress = "127.0.0.1" user.LastSSHLogin = now - 10 usermaint.main() assert user.LastLoginIPAddress == "127.0.0.1" assert user.LastSSHLoginIPAddress == "127.0.0.1"
async def test_basic_auth_backend(user: User, backend: BasicAuthBackend): # This time, everything matches up. We expect the user to # equal the real_user. now_ts = time.utcnow() with db.begin(): db.create(Session, UsersID=user.ID, SessionID="realSession", LastUpdateTS=now_ts + 5) request = Request() request.cookies["AURSID"] = "realSession" _, result = await backend.authenticate(request) assert result == user
def proposal(user, tu_user): ts = time.utcnow() agenda = "Test proposal." start = ts - 5 end = ts + 1000 with db.begin(): voteinfo = db.create(TUVoteInfo, Agenda=agenda, Quorum=0.0, User=user.Username, Submitter=tu_user, Submitted=start, End=end) yield (tu_user, user, voteinfo)
def test_tu_voteinfo_is_running(user: User): ts = time.utcnow() with db.begin(): tu_voteinfo = create(TUVoteInfo, Agenda="Blah blah.", User=user.Username, Submitted=ts, End=ts + 1000, Quorum=0.5, Submitter=user) assert tu_voteinfo.is_running() is True with db.begin(): tu_voteinfo.End = ts - 5 assert tu_voteinfo.is_running() is False
def main(): db.get_engine() now = time.utcnow() start = aurweb.config.getint("tuvotereminder", "range_start") filter_from = now + start end = aurweb.config.getint("tuvotereminder", "range_end") filter_to = now + end query = db.query(TUVoteInfo.ID).filter( and_(TUVoteInfo.End >= filter_from, TUVoteInfo.End <= filter_to)) for voteinfo in query: notif = notify.TUVoteReminderNotification(voteinfo.ID) notif.send()
def packages(maintainer: User) -> List[Package]: """ Yield 55 packages named pkg_0 .. pkg_54. """ packages_ = [] now = time.utcnow() with db.begin(): for i in range(55): pkgbase = db.create(PackageBase, Name=f"pkg_{i}", Maintainer=maintainer, Packager=maintainer, Submitter=maintainer, ModifiedTS=now) package = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") packages_.append(package) yield packages_
def test_homepage_dashboard_flagged_packages(redis, packages, user): # Set the first Package flagged by setting its OutOfDateTS column. pkg = packages[0] with db.begin(): pkg.PackageBase.OutOfDateTS = time.utcnow() cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: response = request.get("/", cookies=cookies) assert response.status_code == int(HTTPStatus.OK) # Check to see that the package showed up in the Flagged Packages table. root = parse_root(response.text) flagged_pkg = root.xpath('//table[@id="flagged-packages"]/tbody/tr').pop(0) flagged_name = flagged_pkg.xpath('./td/a').pop(0) assert flagged_name.text.strip() == pkg.Name