access denied fix
This commit is contained in:
@@ -14,7 +14,7 @@ from flask import (
|
||||
url_for,
|
||||
)
|
||||
|
||||
from ..config import get_dev_fio_password, is_assignment_feature_enabled, is_dev_ui
|
||||
from ..config import get_dev_fio_password, is_assignment_feature_enabled, is_dev_ui, is_hr_auth_enabled
|
||||
from ..messages import RU
|
||||
from .decorators import login_required, current_user
|
||||
from .services import AuthError, authenticate_credentials
|
||||
@@ -50,6 +50,7 @@ def login_page():
|
||||
'auth/login.html',
|
||||
next=_safe_next('/'),
|
||||
dev_fio_enabled=bool(get_dev_fio_password()),
|
||||
hr_auth_enabled=is_hr_auth_enabled(),
|
||||
)
|
||||
|
||||
|
||||
@@ -66,6 +67,7 @@ def login_submit():
|
||||
next=_safe_next('/'),
|
||||
login=login,
|
||||
dev_fio_enabled=bool(get_dev_fio_password()),
|
||||
hr_auth_enabled=is_hr_auth_enabled(),
|
||||
), e.status
|
||||
except Exception:
|
||||
log.exception('login_submit failed')
|
||||
@@ -75,6 +77,7 @@ def login_submit():
|
||||
next=_safe_next('/'),
|
||||
login=login,
|
||||
dev_fio_enabled=bool(get_dev_fio_password()),
|
||||
hr_auth_enabled=is_hr_auth_enabled(),
|
||||
), 500
|
||||
return redirect(_safe_next('/'))
|
||||
|
||||
|
||||
@@ -92,6 +92,39 @@ def _normalize_fio(s: str) -> str:
|
||||
return re.sub(r'\s+', ' ', t).strip()
|
||||
|
||||
|
||||
_FIO_STAFF_SQL = text(
|
||||
"""
|
||||
SELECT id, fio, web_login
|
||||
FROM staff_members
|
||||
WHERE fio IS NOT NULL
|
||||
AND lower(
|
||||
regexp_replace(
|
||||
trim(replace(replace(coalesce(fio, ''), chr(160), ' '), E'\\t', ' ')),
|
||||
'[[:space:]]+', ' ', 'g'
|
||||
)
|
||||
) = :needle
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _staff_rows_by_normalized_fio(hr_conn, fio_raw: str) -> list:
|
||||
"""Строки staff_members с тем же ФИО, что и ввод, после нормализации (регистр не важен)."""
|
||||
fio_needle = _normalize_fio(fio_raw)
|
||||
if not fio_needle:
|
||||
return []
|
||||
needle = fio_needle.lower()
|
||||
return list(hr_conn.execute(_FIO_STAFF_SQL, {'needle': needle}).mappings().all())
|
||||
|
||||
|
||||
def _single_web_login_from_fio(hr_conn, fio_raw: str) -> str | None:
|
||||
"""Если по ФИО найден ровно один сотрудник — его web_login для входа в HR; иначе None."""
|
||||
rows = _staff_rows_by_normalized_fio(hr_conn, fio_raw)
|
||||
if len(rows) != 1:
|
||||
return None
|
||||
wl = (rows[0]['web_login'] or '').strip()
|
||||
return wl or None
|
||||
|
||||
|
||||
def authenticate_credentials(login: str, password: str) -> AuthUser:
|
||||
login = (login or '').strip()
|
||||
password = password or ''
|
||||
@@ -122,25 +155,8 @@ def _authenticate_dev_fio(fio_raw: str) -> AuthUser:
|
||||
if not fio_needle:
|
||||
raise AuthError(401, RU['invalidCredentials'])
|
||||
|
||||
needle = fio_needle.lower()
|
||||
|
||||
with hr_eng.connect() as hr_conn:
|
||||
rows = hr_conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id, fio, web_login
|
||||
FROM staff_members
|
||||
WHERE fio IS NOT NULL
|
||||
AND lower(
|
||||
regexp_replace(
|
||||
trim(replace(replace(coalesce(fio, ''), chr(160), ' '), E'\\t', ' ')),
|
||||
'[[:space:]]+', ' ', 'g'
|
||||
)
|
||||
) = :needle
|
||||
"""
|
||||
),
|
||||
{'needle': needle},
|
||||
).mappings().all()
|
||||
rows = _staff_rows_by_normalized_fio(hr_conn, fio_raw)
|
||||
if len(rows) != 1:
|
||||
raise AuthError(401, RU['invalidCredentials'])
|
||||
s = rows[0]
|
||||
@@ -208,41 +224,51 @@ def _authenticate_via_hr(login: str, password: str) -> AuthUser:
|
||||
if hr_eng is None:
|
||||
raise AuthError(500, RU['hrDatabaseUrlMissing'])
|
||||
|
||||
user_row_sql = text(
|
||||
'SELECT id, username, password_hash, role FROM users '
|
||||
'WHERE LOWER(TRIM(username)) = LOWER(TRIM(:login))'
|
||||
)
|
||||
|
||||
with hr_eng.connect() as hr_conn:
|
||||
u = hr_conn.execute(
|
||||
text(
|
||||
'SELECT id, username, password_hash, role FROM users '
|
||||
'WHERE LOWER(TRIM(username)) = LOWER(TRIM(:login))'
|
||||
),
|
||||
{'login': login},
|
||||
).mappings().first()
|
||||
u = hr_conn.execute(user_row_sql, {'login': login}).mappings().first()
|
||||
if not u:
|
||||
wl = _single_web_login_from_fio(hr_conn, login)
|
||||
if wl:
|
||||
u = hr_conn.execute(user_row_sql, {'login': wl}).mappings().first()
|
||||
|
||||
if not u or not u['password_hash']:
|
||||
raise AuthError(401, RU['invalidCredentials'])
|
||||
if not _verify_password(password, u['password_hash']):
|
||||
raise AuthError(401, RU['invalidCredentials'])
|
||||
|
||||
hr_username = (u['username'] or '').strip()
|
||||
if not hr_username:
|
||||
raise AuthError(401, RU['invalidCredentials'])
|
||||
|
||||
s = hr_conn.execute(
|
||||
text(
|
||||
'SELECT id, fio FROM staff_members '
|
||||
"WHERE LOWER(TRIM(COALESCE(web_login, ''))) = LOWER(TRIM(:login))"
|
||||
),
|
||||
{'login': login},
|
||||
{'login': hr_username},
|
||||
).mappings().first()
|
||||
if not s:
|
||||
raise AuthError(403, RU['noStaffForLogin'])
|
||||
|
||||
staff_id = int(s['id'])
|
||||
fio = s['fio'] or login
|
||||
fio = s['fio'] or hr_username
|
||||
app_role = map_hr_role_to_app(u['role'])
|
||||
canonical_login = hr_username
|
||||
|
||||
# UPSERT через ORM: ищем по staff_id, затем по login, затем создаём
|
||||
# UPSERT через ORM: ищем по staff_id, затем по логину HR, затем по вводу (ФИО/старый логин)
|
||||
session = get_session()
|
||||
user = session.query(User).filter(User.staff_id == staff_id).first()
|
||||
if not user:
|
||||
# при первом входе staff_id ещё не проставлен — ищем по login
|
||||
user = session.query(User).filter(User.login == canonical_login).first()
|
||||
if not user:
|
||||
user = session.query(User).filter(User.login == login).first()
|
||||
if user:
|
||||
user.login = login
|
||||
user.login = canonical_login
|
||||
user.full_name = fio
|
||||
user.role = app_role
|
||||
user.password_hash = HR_MANAGED_PASSWORD_PLACEHOLDER
|
||||
@@ -250,7 +276,7 @@ def _authenticate_via_hr(login: str, password: str) -> AuthUser:
|
||||
user.is_active = True
|
||||
else:
|
||||
user = User(
|
||||
login=login,
|
||||
login=canonical_login,
|
||||
password_hash=HR_MANAGED_PASSWORD_PLACEHOLDER,
|
||||
full_name=fio,
|
||||
role=app_role,
|
||||
|
||||
@@ -38,6 +38,10 @@ def user_has_test_access(user_id: str, test_id: str) -> AccessResult:
|
||||
if is_test_author(test.created_by, uid):
|
||||
return AccessResult(ok=True, is_author=True, not_found=False)
|
||||
|
||||
# Глобальная политика доступа: любой активный тест доступен всем сотрудникам.
|
||||
if bool(test.is_active):
|
||||
return AccessResult(ok=True, is_author=False, not_found=False)
|
||||
|
||||
assigned = session.query(
|
||||
exists().where(
|
||||
TestAssignmentTarget.target_type == 'user',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% block title %}Вход — Тестирование{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set fio_as_login = dev_fio_enabled or hr_auth_enabled %}
|
||||
{% if ui_variant == 'legacy' %}
|
||||
<div class="login-page">
|
||||
<div class="login-shell">
|
||||
@@ -26,17 +27,21 @@
|
||||
<p style="font-size:0.8rem; color:#4b7b78; margin-bottom:0.75rem; line-height:1.4;">
|
||||
Введите <b>ФИО</b> из кадровой системы и общий dev-пароль — или обычный логин/пароль.
|
||||
</p>
|
||||
{% elif hr_auth_enabled %}
|
||||
<p style="font-size:0.8rem; color:#4b7b78; margin-bottom:0.75rem; line-height:1.4;">
|
||||
Можно ввести <b>логин</b> из HR или <b>ФИО</b> (как в кадровой системе), если совпадение одно, и пароль учётной записи HR.
|
||||
</p>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('auth.login_submit') }}" novalidate>
|
||||
<input type="hidden" name="next" value="{{ next or '/' }}">
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="login-username">
|
||||
{% if dev_fio_enabled %}ФИО или логин{% else %}Логин{% endif %}
|
||||
{% if fio_as_login %}ФИО или логин{% else %}Логин{% endif %}
|
||||
</label>
|
||||
<input id="login-username" class="form-input" type="text" name="login"
|
||||
value="{{ login or '' }}" required autofocus autocomplete="username"
|
||||
placeholder="{% if dev_fio_enabled %}Иванов Иван Иванович{% endif %}" />
|
||||
placeholder="{% if fio_as_login %}Иванов Иван Иванович{% endif %}" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
@@ -61,7 +66,7 @@
|
||||
{% if dev_fio_enabled %}
|
||||
Введите <b>ФИО</b> из кадровой системы и общий dev-пароль — или обычный логин/пароль.
|
||||
{% elif hr_auth_enabled %}
|
||||
Учётка кадровой системы (HR).
|
||||
Учётная запись HR: можно ввести <b>логин</b> или <b>ФИО</b> (если в базе только один такой сотрудник), и пароль.
|
||||
{% else %}
|
||||
Используйте логин и пароль.
|
||||
{% endif %}
|
||||
@@ -86,10 +91,10 @@
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-ink-700">
|
||||
{% if dev_fio_enabled %}ФИО или логин{% else %}Логин{% endif %}
|
||||
{% if fio_as_login %}ФИО или логин{% else %}Логин{% endif %}
|
||||
</span>
|
||||
<input type="text" name="login" value="{{ login or '' }}" required autofocus autocomplete="username"
|
||||
placeholder="{% if dev_fio_enabled %}Иванов Иван Иванович{% endif %}"
|
||||
placeholder="{% if fio_as_login %}Иванов Иван Иванович{% endif %}"
|
||||
class="mt-1 w-full rounded-lg border border-ink-300 bg-white px-3 py-2 text-ink-900
|
||||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20" />
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user