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/build-base-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
echo "${{ env.IMGVERSION }}" > dev/build/TARGET_BASE

- name: Commit CHANGELOG.md
uses: stefanzweifel/git-auto-commit-action@v5
uses: stefanzweifel/git-auto-commit-action@v6
with:
branch: ${{ github.ref_name }}
commit_message: 'ci: update base image target version to ${{ env.IMGVERSION }}'
Expand Down
2 changes: 1 addition & 1 deletion dev/build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/ietf-tools/datatracker-app-base:20250514T1627
FROM ghcr.io/ietf-tools/datatracker-app-base:20250624T1543
LABEL maintainer="IETF Tools Team <tools-discuss@ietf.org>"

ENV DEBIAN_FRONTEND=noninteractive
Expand Down
2 changes: 1 addition & 1 deletion dev/build/TARGET_BASE
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20250514T1627
20250624T1543
39 changes: 31 additions & 8 deletions docker/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Datatracker Development in Docker

- [Getting started](#getting-started)
- [Using Visual Studio Code](#using-visual-studio-code)
- [Initial Setup](#initial-setup)
- [Subsequent Launch](#subsequent-launch)
- [Usage](#usage)
- [Using Other Editors / Generic](#using-other-editors--generic)
- [Exit Environment](#exit-environment)
- [Accessing PostgreSQL Port](#accessing-postgresql-port)
- [Clean and Rebuild DB from latest image](#clean-and-rebuild-db-from-latest-image)
- [Clean all](#clean-all)
- [Updating an older environment](#updating-an-older-environment)
- [Notes / Troubleshooting](#notes--troubleshooting)

## Getting started

1. [Set up Docker](https://docs.docker.com/get-started/) on your preferred platform. On Windows, it is highly recommended to use the [WSL 2 *(Windows Subsystem for Linux)*](https://docs.docker.com/desktop/windows/wsl/) backend.
Expand Down Expand Up @@ -123,7 +136,14 @@ docker compose down

to terminate the containers.

### Clean and Rebuild DB from latest image
### Accessing PostgreSQL Port

The port is exposed but not automatically mapped to `5432` to avoid potential conflicts with the host. To get the mapped port, run the command *(from the project `/docker` directory)*:
```sh
docker compose port db 5432
```

## Clean and Rebuild DB from latest image

To delete the active DB container, its volume and get the latest image / DB dump, simply run the following command:

Expand All @@ -141,7 +161,7 @@ docker compose pull db
docker compose build --no-cache db
```

### Clean all
## Clean all

To delete all containers for this project, its associated images and purge any remaining dangling images, simply run the following command:

Expand All @@ -157,17 +177,20 @@ On Windows:
docker compose down -v --rmi all
docker image prune
```
### Updating an older environment

## Updating an older environment

If you already have a clone, such as from a previous codesprint, and are updating that clone, before starting the datatracker from the updated image:
* rm ietf/settings_local.py # The startup script will put a new one, appropriate to the current release, in place
* Execute the `Clean all` sequence above.
1. `rm ietf/settings_local.py` *(The startup script will put a new one, appropriate to the current release, in place)*
1. Execute the [Clean all](#clean-all) sequence above.

### Accessing PostgreSQL Port
If the dev environment fails to start, even after running the [Clean all](#clean-all) sequence above, you can fully purge all docker cache, containers, images and volumes by running the command below.

> [!CAUTION]
> Note that this will delete everything docker-related, including non-datatracker docker resources you might have.

The port is exposed but not automatically mapped to `5432` to avoid potential conflicts with the host. To get the mapped port, run the command *(from the project `/docker` directory)*:
```sh
docker compose port db 5432
docker system prune -a --volumes
```

## Notes / Troubleshooting
Expand Down
192 changes: 192 additions & 0 deletions ietf/group/migrations/0005_remove_sdo_authorized_individuals.py

Large diffs are not rendered by default.

270 changes: 270 additions & 0 deletions ietf/group/migrations/0006_remove_liason_contacts.py

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions ietf/group/migrations/0007_used_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright The IETF Trust 2025, All Rights Reserved

from django.db import migrations


def forward(apps, schema_editor):
Group = apps.get_model("group", "Group")
GroupFeatures = apps.get_model("group", "GroupFeatures")
iab = Group.objects.get(acronym="iab")
iab.used_roles = [
"chair",
"delegate",
"exofficio",
"liaison",
"liaison_coordinator",
"member",
]
iab.save()
GroupFeatures.objects.filter(type_id="ietf").update(
default_used_roles=[
"ad",
"member",
"comdir",
"delegate",
"execdir",
"recman",
"secr",
"chair",
]
)


def reverse(apps, schema_editor):
Group = apps.get_model("group", "Group")
iab = Group.objects.get(acronym="iab")
iab.used_roles = []
iab.save()
# Intentionally not putting trac-* back into grouptype ietf default_used_roles


class Migration(migrations.Migration):
dependencies = [
("group", "0006_remove_liason_contacts"),
("name", "0018_alter_rolenames"),
]

operations = [
migrations.RunPython(forward, reverse),
]
54 changes: 38 additions & 16 deletions ietf/ietfauth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@


import re

from unidecode import unidecode

from django import forms
from django.contrib.auth.models import User
from django.contrib.auth import password_validation
from django.core.exceptions import ValidationError
from django.db import models
from django.contrib.auth.models import User

from ietf.person.models import Person, Email
from ietf.mailinglists.models import Allowlisted
from ietf.utils.text import isascii
from .password_validation import StrongPasswordValidator

from .validators import prevent_at_symbol, prevent_system_name, prevent_anonymous_name, is_allowed_address
from .widgets import PasswordStrengthInput, PasswordConfirmationInput
Expand Down Expand Up @@ -170,33 +173,52 @@ class Meta:
model = Allowlisted
exclude = ['by', 'time' ]


from django import forms


class ChangePasswordForm(forms.Form):
current_password = forms.CharField(widget=forms.PasswordInput)

new_password = forms.CharField(widget=PasswordStrengthInput(attrs={'class':'password_strength'}))
new_password_confirmation = forms.CharField(widget=PasswordConfirmationInput(
confirm_with='new_password',
attrs={'class':'password_confirmation'}))
new_password = forms.CharField(
widget=PasswordStrengthInput(
attrs={
"class": "password_strength",
"data-disable-strength-enforcement": "", # usually removed in init
}
),
)
new_password_confirmation = forms.CharField(
widget=PasswordConfirmationInput(
confirm_with="new_password", attrs={"class": "password_confirmation"}
)
)

def __init__(self, user, data=None):
self.user = user
super(ChangePasswordForm, self).__init__(data)
super().__init__(data)
# Check whether we have validators to enforce
new_password_field = self.fields["new_password"]
for pwval in password_validation.get_default_password_validators():
if isinstance(pwval, password_validation.MinimumLengthValidator):
new_password_field.widget.attrs["minlength"] = pwval.min_length
elif isinstance(pwval, StrongPasswordValidator):
new_password_field.widget.attrs.pop(
"data-disable-strength-enforcement", None
)

def clean_current_password(self):
password = self.cleaned_data.get('current_password', None)
# n.b., password = None is handled by check_password and results in a failed check
password = self.cleaned_data.get("current_password", None)
if not self.user.check_password(password):
raise ValidationError('Invalid password')
raise ValidationError("Invalid password")
return password

def clean(self):
new_password = self.cleaned_data.get('new_password', None)
conf_password = self.cleaned_data.get('new_password_confirmation', None)
if not new_password == conf_password:
raise ValidationError("The password confirmation is different than the new password")
new_password = self.cleaned_data.get("new_password", "")
conf_password = self.cleaned_data.get("new_password_confirmation", "")
if new_password != conf_password:
raise ValidationError(
"The password confirmation is different than the new password"
)
password_validation.validate_password(conf_password, self.user)


class ChangeUsernameForm(forms.Form):
Expand Down
23 changes: 23 additions & 0 deletions ietf/ietfauth/password_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright The IETF Trust 2025, All Rights Reserved
from django.core.exceptions import ValidationError
from zxcvbn import zxcvbn


class StrongPasswordValidator:
message = "This password does not meet complexity requirements and is easily guessable."
code = "weak"
min_zxcvbn_score = 3

def __init__(self, message=None, code=None, min_zxcvbn_score=None):
if message is not None:
self.message = message
if code is not None:
self.code = code
if min_zxcvbn_score is not None:
self.min_zxcvbn_score = min_zxcvbn_score

def validate(self, password, user=None):
"""Validate that a password is strong enough"""
strength_report = zxcvbn(password[:72], max_length=72)
if strength_report["score"] < self.min_zxcvbn_score:
raise ValidationError(message=self.message, code=self.code)
Loading