Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/hypha-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version-file: ".python-version"

Expand Down Expand Up @@ -58,13 +58,13 @@ jobs:
group: [1, 2, 3]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version-file: ".python-version"

- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "requirements**.txt"
Expand Down
12 changes: 6 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
rev: "0.6.14"
rev: "0.9.22"
hooks:
- id: uv-export
name: uv-export requirements/prod.txt
Expand Down Expand Up @@ -36,7 +36,7 @@ repos:
]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.11.6"
rev: "v0.14.10"
hooks:
# Run the linter.
- id: ruff
Expand All @@ -45,7 +45,7 @@ repos:
- id: ruff-format

- repo: https://github.com/rtts/djhtml
rev: "3.0.7"
rev: "3.0.10"
hooks:
- id: djhtml
files: .*/templates/.*\.html$
Expand All @@ -62,7 +62,7 @@ repos:
types_or: [html, css]

- repo: https://github.com/biomejs/pre-commit
rev: "v2.0.0-beta.1"
rev: "v2.3.10"
hooks:
- id: biome-check
additional_dependencies: ["@biomejs/[email protected]"]
Expand All @@ -77,11 +77,11 @@ repos:
- [email protected]
- [email protected]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.3
rev: v8.30.0
hooks:
- id: gitleaks

- repo: https://github.com/crate-ci/typos
rev: v1.31.1
rev: v1.41.0
hooks:
- id: typos
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12.11
3.12
8 changes: 4 additions & 4 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.12.11-bookworm
FROM python:3.12-bookworm

# Add venv/bin to PATH.
ENV PATH="/opt/app/.venv/bin:/usr/local/bin:$PATH"
Expand All @@ -10,11 +10,11 @@ ENV UV_LINK_MODE=copy
WORKDIR /opt/app

# Install node.
COPY --from=node:24.1-slim /usr/local/bin /usr/local/bin
COPY --from=node:24.1-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node:24-slim /usr/local/bin /usr/local/bin
COPY --from=node:24-slim /usr/local/lib/node_modules /usr/local/lib/node_modules

# Install uv.
COPY --from=ghcr.io/astral-sh/uv:0.5.13 /uv /uvx /usr/local/bin
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin

# Install node dependencies.
COPY package*.json ./
Expand Down
10 changes: 5 additions & 5 deletions docker/prod/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# Builder stage.
#
FROM python:3.12.11-bookworm AS builder
FROM python:3.12-bookworm AS builder

# Add venv/bin to PATH.
ENV PATH="/opt/app/.venv/bin:/usr/local/bin:$PATH"
Expand All @@ -19,11 +19,11 @@ WORKDIR /opt/app
RUN mkdir -p ./hypha/static_compiled && mkdir -p ./hypha/media

# Install node.
COPY --from=node:24.1-slim /usr/local/bin /usr/local/bin
COPY --from=node:24.1-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node:24-slim /usr/local/bin /usr/local/bin
COPY --from=node:24-slim /usr/local/lib/node_modules /usr/local/lib/node_modules

# Install uv.
COPY --from=ghcr.io/astral-sh/uv:0.5.13 /uv /uvx /usr/local/bin
COPY --from=ghcr.io/astral-sh/uv:0.5.36 /uv /uvx /usr/local/bin

# Install node dependencies.
COPY package*.json ./
Expand All @@ -45,7 +45,7 @@ RUN npm run build && python manage.py collectstatic --noinput --verbosity=0
#
# Production stage.
#
FROM python:3.12.11-slim-bookworm
FROM python:3.12-slim-bookworm

# Add venv/bin to PATH.
ENV PATH="/opt/app/.venv/bin:/usr/local/bin:$PATH"
Expand Down
6 changes: 3 additions & 3 deletions docs/references/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Once an application is submitted (`INITIAL_STATE`) — it can transition into th

### 👳 Request with same time review

This workflow is a single stage process with an advisory council review or external review stage -- includes functionalties for external reviewers like advisory board members to access applications and submit reviews.
This workflow is a single stage process with an advisory council review or external review stage -- includes functionalities for external reviewers like advisory board members to access applications and submit reviews.

It is very similar to the "Request with external review" workflow, see below, but the internal and external review step happens at the same step.

Expand All @@ -62,7 +62,7 @@ Once an application is submitted (`INITIAL_STATE`) — it can transition into th

### 👳 Request with external review

This workflow is a single stage process with an advisory council review or external review stage -- includes functionalties for external reviewers like advisory board members to access applications and submit reviews.
This workflow is a single stage process with an advisory council review or external review stage -- includes functionalities for external reviewers like advisory board members to access applications and submit reviews.

Proposal Persona: This funding organization relies on external partners for evaluations. Proposals submitted to this workflow are reviewed by staff members and an advisory board that is made up of trusted community members.

Expand All @@ -86,7 +86,7 @@ Once an application is submitted (`INITIAL_STATE`) — it can transition into th

### 👪 Request with community review

This workflow is a single stage application process with functionalties for external reviewers, including applicants to carry out peer review of each other applications.
This workflow is a single stage application process with functionalities for external reviewers, including applicants to carry out peer review of each other applications.

**Proposal Persona:**

Expand Down
3 changes: 2 additions & 1 deletion hypha/apply/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from django.urls import include, path, reverse_lazy
from django.views.generic import RedirectView
from django_ratelimit.decorators import ratelimit
from elevate.views import elevate as elevate_view

from hypha.elevate.views import elevate as elevate_view

from .views import (
AccountView,
Expand Down
6 changes: 3 additions & 3 deletions hypha/apply/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@
from django_htmx.http import HttpResponseClientRedirect
from django_otp import devices_for_user
from django_ratelimit.decorators import ratelimit
from elevate.mixins import ElevateMixin
from elevate.utils import grant_elevated_privileges
from elevate.views import redirect_to_elevate
from hijack.views import AcquireUserView
from social_django.utils import psa
from social_django.views import complete
Expand All @@ -54,6 +51,9 @@
from wagtail.users.views.users import change_user_perm

from hypha.core.mail import MarkdownMail
from hypha.elevate.mixins import ElevateMixin
from hypha.elevate.utils import grant_elevated_privileges
from hypha.elevate.views import redirect_to_elevate

from .decorators import require_oauth_whitelist
from .forms import (
Expand Down
20 changes: 20 additions & 0 deletions hypha/elevate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
elevate
~~~~

:copyright: (c) 2017-present by Justin Mayer.
:copyright: (c) 2014-2016 by Matt Robenolt.
:license: BSD, see LICENSE for more details.

2026-01-07: Moved in to hypha to avoid Setuptools complaining
about use of obsolete pkg_resources.
"""

from importlib.metadata import version

try:
VERSION = version("elevate")
except Exception: # pragma: no cover
VERSION = "unknown"

default_app_config = "hypha.elevate.apps.ElevateConfig"
18 changes: 18 additions & 0 deletions hypha/elevate/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
elevate.apps
~~~~~~~~~~~~~~

:copyright: (c) 2017-present by Justin Mayer.
:license: BSD, see LICENSE for more details.
"""

from django.apps import AppConfig


class ElevateConfig(AppConfig):
name = "hypha.elevate"
verbose_name = "Django Elevate"

def ready(self):
# register signals
import hypha.elevate.signals # noqa
31 changes: 31 additions & 0 deletions hypha/elevate/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
elevate.decorators
~~~~~~~~~~~~~~~~~~

:copyright: (c) 2017-present by Justin Mayer.
:copyright: (c) 2014-2016 by Matt Robenolt.
:license: BSD, see LICENSE for more details.
"""

from functools import wraps

from .views import redirect_to_elevate


def elevate_required(func):
"""
Enforces a view to have elevated privileges.
Should likely be paired with ``@login_required``.

>>> @elevate_required
>>> def secure_page(request):
>>> ...
"""

@wraps(func)
def inner(request, *args, **kwargs):
if not request.is_elevated():
return redirect_to_elevate(request.get_full_path())
return func(request, *args, **kwargs)

return inner
34 changes: 34 additions & 0 deletions hypha/elevate/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
elevate.forms
~~~~~~~~~~~~~

:copyright: (c) 2017-present by Justin Mayer.
:copyright: (c) 2014-2016 by Matt Robenolt.
:license: BSD, see LICENSE for more details.
"""

from django import forms
from django.contrib import auth
from django.utils.translation import gettext_lazy as _


class ElevateForm(forms.Form):
"""
A simple password input form used by the default :func:`~elevate.views.elevate` view."""

password = forms.CharField(
label=_("Password"), widget=forms.PasswordInput(attrs={"autofocus": True})
)

def __init__(self, request, user, *args, **kwargs):
self.request = request
self.user = user
super().__init__(*args, **kwargs)

def clean_password(self):
username = self.user.get_username()
if auth.authenticate(
request=self.request, username=username, password=self.data["password"]
):
return self.data["password"]
raise forms.ValidationError(_("Incorrect password"))
69 changes: 69 additions & 0 deletions hypha/elevate/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
elevate.middleware
~~~~~~~~~~~~~~~~~~

:copyright: (c) 2017-present by Justin Mayer.
:copyright: (c) 2014-2016 by Matt Robenolt.
:license: BSD, see LICENSE for more details.
"""

from django.utils.deprecation import MiddlewareMixin

from .settings import (
COOKIE_DOMAIN,
COOKIE_HTTPONLY,
COOKIE_NAME,
COOKIE_PATH,
COOKIE_SALT,
COOKIE_SECURE,
)
from .utils import has_elevated_privileges


class ElevateMiddleware(MiddlewareMixin):
"""
Middleware that contributes ``request.is_elevated()`` and sets the required
cookie for Elevate mode to work correctly.
"""

def has_elevated_privileges(self, request):
# Override me to alter behavior
return has_elevated_privileges(request)

def process_request(self, request):
assert hasattr(request, "session"), (
"The Elevate middleware requires session middleware to be installed."
"Edit your MIDDLEWARE_CLASSES setting to insert "
"'django.contrib.sessions.middleware.SessionMiddleware' before "
"'hypha.elevate.middleware.ElevateMiddleware'."
)
request.is_elevated = lambda: self.has_elevated_privileges(request)

def process_response(self, request, response):
is_elevated = getattr(request, "_elevate", None)

if is_elevated is None:
return response

# We have explicitly had Elevate revoked, so clean up cookie
if is_elevated is False and COOKIE_NAME in request.COOKIES:
response.delete_cookie(COOKIE_NAME)
return response

# Elevate mode has been granted,
# and we have a token to send back to the user agent
if is_elevated is True and hasattr(request, "_elevate_token"):
token = request._elevate_token
max_age = request._elevate_max_age
response.set_signed_cookie(
COOKIE_NAME,
token,
salt=COOKIE_SALT,
max_age=max_age, # If max_age is None, it's a session cookie
secure=request.is_secure() if COOKIE_SECURE is None else COOKIE_SECURE,
httponly=COOKIE_HTTPONLY, # Not accessible by JavaScript
path=COOKIE_PATH,
domain=COOKIE_DOMAIN,
)

return response
8 changes: 8 additions & 0 deletions hypha/elevate/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from .decorators import elevate_required


class ElevateMixin:
@classmethod
def as_view(cls, **initkwargs):
view = super().as_view(**initkwargs)
return elevate_required(view)
Loading
Loading