Skip to content

Add password validation for email auth - #265

Open
Georgon wants to merge 1 commit into
mainfrom
email-password-fix
Open

Add password validation for email auth#265
Georgon wants to merge 1 commit into
mainfrom
email-password-fix

Conversation

@Georgon

@Georgon Georgon commented Aug 13, 2026

Copy link
Copy Markdown

Введена новая политика пароля:

  • 8–32 символов

Разрешены:

  • A-Z, a-z, 0-9 и стандартные ASCII-спецсимволы, например: ! @ # $ % ^ & * ( ) _ + - = [ ] { } ; : , . / ? и т. д.

Запрещены пробелы, \n, \t, кириллица, буквы с диакритикой и вообще символы вне ASCII.

При этом я специально не применял новые ограничения к паролю при логине и к полю старого пароля при смене. Это важно для обратной совместимости: пользователь, зарегистрированный раньше с паролем вроде пароль123, по-прежнему сможет войти. Но установить такой пароль заново уже нельзя.

Основные изменения хранятся в auth_backend/schemas/types/password.py - новый файл, где централизовано хранятся условия на пароль: PASSWORD_MIN_LENGTH = 8, PASSWORD_MAX_LENGTH = 32, набор разрешённых символов, validate_password() и Pydantic-тип Password. В OpenAPI добавлены minLength, maxLength, pattern, format=password и текстовое описание требований.
Создание пользователя через CLI теперь тоже не позволяет обойти политику пароля.
Добавлены новые тесты: tests/test_unit/test_password.py - проверяются длина, Unicode, пробел, \n, \t, все ASCII-спецсимволы и данные, которые попадут в OpenAPI; tests/test_routes/test_registration.py и test_change_password.py - добавлены проверки, что короткие и недопустимые пароли дают 422.

Также изменен README и Makefile под новые правила пароля.

Closes #246

@Georgon Georgon self-assigned this Aug 13, 2026
@Georgon
Georgon requested a review from petrCher August 13, 2026 18:23
@github-actions

Copy link
Copy Markdown

Code Coverage

Coverage Report
FileStmtsMissCoverMissing
auth_backend
   __main__.py330%1–4
   base.py12467%6–9
   exceptions.py45687%32, 50–52, 57, 78
auth_backend/admin
   admin.py973762%17, 38, 41, 44–48, 51–55, 58–60, 91, 94, 97–102, 105–113, 116–117, 147, 150, 153–156
   auth.py412929%14–24, 27–31, 34–35, 39–49
   filter.py171135%13–24
auth_backend/auth_method
   base.py63494%51, 53, 60, 142
   method_mixins.py32294%27, 50
   oauth.py561770%37, 43, 48–53, 57–68, 94–99
   outer.py1142975%26, 33, 40, 47, 98, 104, 113, 167–172, 185–193, 205–211
   userdata_mixin.py19574%32, 37–40
auth_backend/auth_plugins
   airflow.py331652%25–36, 41–64
   authentic.py19010346%52–64, 69–82, 86–101, 105–111, 115–121, 128–130, 140–196, 203–253, 258, 263–264, 274–281, 310–316, 325, 330–340, 345–358
   coder.py321650%24–33, 38–51
   email.py2561495%91, 248, 333, 335, 340, 351–357, 404, 445, 496, 506
   github.py1046339%56–115, 129–173, 183, 188, 194–206
   google.py1126443%64–123, 137–165, 175, 181–185, 189–194, 198–207
   keycloak.py1046240%52–114, 124–174, 184, 189, 195–200
   lkmsu.py16411232%59–112, 130–168, 178–200, 205, 210, 216–226, 230–244, 248–277, 281–290
   mailu.py341847%24–33, 38–53
   postgres.py432053%28–34, 39–45, 50–62
   telegram.py1035448%57–84, 99–117, 130, 138, 146–166, 171–185
   vk.py1288236%70–122, 136–174, 184, 189, 195–212, 219–245
   yandex.py1278335%57–127, 140–178, 189, 195, 201–227
auth_backend/cli
   group.py13130%1–17
   process.py56560%1–71
   scope.py11110%1–20
   user.py22220%1–42
   user_group.py12120%1–25
auth_backend/kafka
   kafka.py502060%29–32, 41–43, 55–58, 71–81, 84, 92, 102
   kafkameta.py9278%8, 12
auth_backend/models
   base.py83594%79, 100–102, 106
   db.py152895%46, 55, 59, 120, 214, 223, 227, 240
   dynamic_settings.py27774%24–31
auth_backend/routes
   base.py37295%25–26
   exc_handlers.py43686%31, 56, 69, 95, 117, 125
   groups.py92397%81, 148, 150
   oidc.py34197%93
   scopes.py39392%16, 55–56
   user.py972772%52, 54, 56–65, 71–77, 89–109, 162, 172
   user_session.py84693%79–88, 118, 120, 181, 188
auth_backend/schemas
   models.py100397%150–152
auth_backend/schemas/types
   scopes.py24675%30–32, 37, 40, 42
auth_backend/utils
   jwt.py57788%34–41
   oidc_token.py37878%26, 39–43, 48, 74
   security.py46296%40, 65
   smtp.py781976%75–78, 100–120
   string.py7186%12
   user_session_control.py41295%32, 40
TOTAL3336110667% 

Summary

Tests Skipped Failures Errors Time
82 6 💤 0 ❌ 0 🔥 25.330s ⏱️

@petrCher petrCher removed this from Viribus Team Aug 14, 2026
@petrCher

petrCher commented Aug 15, 2026

Copy link
Copy Markdown
Member

сделай git rebase, надо подтянуть изменения с коммита Артема, который я замерджил вчера

p.s. можно и через кнопку update branch (если она есть у тебя, находится внизу пр рядом с галочками all checks passed) сделать это, но тогда проверь что конфликтов кода не должно быть с последним мерджем

@petrCher petrCher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

отревьюил пока что без тестов, в целом можешь уже начать менять, ревью тестов сейчас тоже уже начну

"""Validate a newly created password according to the Auth API password policy."""
if len(value) < PASSWORD_MIN_LENGTH:
raise ValueError(f"Password must be at least {PASSWORD_MIN_LENGTH} characters long")
if len(value) > PASSWORD_MAX_LENGTH:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь лучше elif для более быстрой работы, если len(value)<min то больше max точно не будет

from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema

PASSWORD_MIN_LENGTH = 8

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

надо эту переменную перенести в settings и здесь ее просто оттуда импортировать
в settings надо, так как все переменные окружения лежат там, чтобы можно было их не прописывать вручную в коде, а дать возможность не меняя код в секретах репозитория в environments values задать то, что хочется. Конечно, можно и не задавать свои значения будет, тогда возьмутся значения дефолта, то есть =8 как ты и пропишешь

также, когда перенесешь это в settings надо еще в github/workflows в test и prod прописать аналогичные команды как здесь https://github.com/profcomff/auth-api/blob/main/.github/workflows/build_and_publish.yml#L147

from pydantic_core import core_schema

PASSWORD_MIN_LENGTH = 8
PASSWORD_MAX_LENGTH = 32

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

с этой переменной аналогично

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ниже переменные (с 10 строчки) оставить здесь, их нет смысла переносить в settings

Comment thread README.md
```

Требования к новому паролю:
- длина от 8 до 32 символов;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

от min до max
надо вместо цифр прописать переменные

Comment thread README.md
```

Требования к новому паролю:
- длина от 8 до 32 символов;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

от min до max
надо вместо цифр прописать переменные

PASSWORD_MIN_LENGTH = 8
PASSWORD_MAX_LENGTH = 32
PASSWORD_ALLOWED_CHARACTERS = string.ascii_letters + string.digits + string.punctuation
PASSWORD_PATTERN = r"^[\x21-\x7E]+$"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а зачем эта константа если есть PASSWORD_ALLOWED_CHARACTERS

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ты ее в валидации используешь, ее же и используй в классе Password, не надо плодить константы одинаковые по сути

Comment thread auth_backend/cli/user.py


def create_user(email: str, password: str, session: Session) -> None:
password = validate_password(password)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь можно без присваивания, ты же в validate_password никак не меняешь пароль, а всего лишь проверяешь

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

плюс в коде ниже есть проверка с print и exit
здесь лучше тоже подобное поставить, чтобы отловить проблему если что

Comment thread Makefile

create-user:
python -m auth_backend user create --email test-user@profcomff.com --password string
python -m auth_backend user create --email test-user@profcomff.com --password string12

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

мб коммент добавить сюда и в изменение ниже, что пароль должен удовлетворять min и max, просто при беглом изучении может быть неочевидно почему именно string12 а не просто string к примеру

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

вроде коммент здесь через хэштег

Comment thread Makefile

create-admin:
source ./venv/bin/activate && python -m auth_backend user create --email test-admin@profcomff.com --password string
source ./venv/bin/activate && python -m auth_backend user create --email test-admin@profcomff.com --password string12

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь соответственно тоже

@petrCher petrCher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

все отревьюил, можно исправлять)

def user_id(client_auth: TestClient, dbsession):
time = datetime.datetime.utcnow()
body = {"email": f"user{time}@example.com", "password": "string"}
body = {"email": f"user{time}@example.com", "password": "string12"}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

я бы лучше что-то более существенное прописал сюда, например "test-password"

везде ниже тоже

f"{url}/request",
headers={"Authorization": auth_token},
json={"password": "", "new_password": "changed"},
json={"password": "", "new_password": "changed12"},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь какой нибудь "changed-pass"

def test_invalid_password(client_auth: TestClient):
invalid_passwords = [
"short7",
"a" * 129,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

129 уже не сильно актуально)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

лучше делать PASSWORD_MAX_LENGTH + 1


def test_invalid_password(client_auth: TestClient):
invalid_passwords = [
"short7",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь лучше тоже как ниже через min делать строку, чтобы не быть привязанным к чему-то конкретному


def test_invalid_password(client_auth: TestClient):
invalid_passwords = [
"short7",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

здесь лучше тоже как ниже через min делать строку, чтобы не быть привязанным к чему-то конкретному



def test_password_accepts_all_ascii_punctuation():
value = string.punctuation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image клод такое подсказал, я бы не заметил)

PasswordModel(password=value)


def test_password_json_schema_contains_frontend_requirements():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а зачем этот тест не очень понял? разве здесь не будет всегда assert отправлять положительный ответ? ну просто я так понимаю ты кладешь все эти переменные еще в password.py и сейчас сравниваешь сами с собой, в чем я не прав?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Поле пароль при регистрации не имеет требований

2 participants