Правки различные

This commit is contained in:
mi
2026-08-26 11:05:32 +03:00
parent c1e49fb15d
commit 728b9826a3
36 changed files with 722 additions and 1986 deletions
+2 -1
View File
@@ -89,8 +89,9 @@ IDGTL_SMS_CALLBACK_PUBLIC_URL=https://chat.example.ru/callbacks/idgtl/sms
BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080
BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox
BITRIX_API_FORWARD_URL=http://api-backend:8000/internal/openlines/v1/inbox
# Example only: set the actual VM2 private DNS name in the deployment .env.
MESSAGE_SAFETY_URL=https://processing.internal:8443
# Docker extra_hosts mapping for VM2 private listener: <hostname>=<private-ip>.
# Docker extra_hosts mapping must use the same configured hostname: <hostname>=<private-ip>.
MESSAGE_SAFETY_EXTRA_HOST=processing.internal=192.168.0.4
MESSAGE_SAFETY_CA_HOST_PATH=/etc/han/ca/vm2-internal-ca.pem
MESSAGE_SAFETY_API_PREFIX=/internal/safety/v2
@@ -59,8 +59,9 @@ han-notification-draft-cleanup-worker
Токены генерируются `openssl rand -hex 32`. `MESSAGE_SAFETY_SERVICE_TOKEN`
сохраняется как caller secret API backend; S3 credentials сервису Safety не
передаются. В production подключение PostgreSQL должно использовать TLS. Target
`MESSAGE_SAFETY_URL=https://processing.internal:8443`, API prefix
`/internal/safety/v2`; certificate проверяется по CA из
задаётся через `MESSAGE_SAFETY_URL=https://<private-vm2-name>:8443`
(`processing.internal` — только пример), API prefix `/internal/safety/v2`;
certificate проверяется по CA из
`MESSAGE_SAFETY_CA_HOST_PATH`, plaintext HTTP запрещён.
Smoke-сценарий `producer_test`: отправить `POST
+4 -2
View File
@@ -51,8 +51,10 @@ non-secret `PUBLIC_WEB_URL` environment variable. Keycloak resolves the
initial `--import-realm`.
Use exact Expo universal/app links and web origins. Do not replace them with
wildcards. `${PUBLIC_WEB_URL}/auth/callback` and `han-chat://auth/callback` are
allow-listed by the initial realm import.
wildcards. `${PUBLIC_WEB_URL}/auth/callback`, `${PUBLIC_WEB_URL}/mobile/oidc/callback`
and `han-chat://auth/callback` are allow-listed by the initial realm import.
Android Custom Tabs cannot follow a custom-scheme 302, so the mobile client uses
the HTTPS bridge page and then opens `han-chat://auth/callback`.
The JDBC URL must use the managed PostgreSQL private endpoint, TLS verification and `currentSchema=keycloak`. The database role must have privileges only on schema `keycloak`.
@@ -61,6 +61,7 @@
"fullScopeAllowed": false,
"redirectUris": [
"${PUBLIC_WEB_URL}/auth/callback",
"${PUBLIC_WEB_URL}/mobile/oidc/callback",
"han-chat://auth/callback"
],
"webOrigins": [
@@ -29,6 +29,7 @@ class RealmContractTest {
assertTrue(realm.contains("\"optionalClientScopes\": [\"offline_access\"]"));
assertTrue(realm.contains("\"han-chat://auth/callback\""));
assertTrue(realm.contains("\"${PUBLIC_WEB_URL}/auth/callback\""));
assertTrue(realm.contains("\"${PUBLIC_WEB_URL}/mobile/oidc/callback\""));
assertTrue(realm.contains("\"${PUBLIC_WEB_URL}\""));
assertFalse(realm.contains("chat.han0107.ru"));
}
@@ -18,7 +18,7 @@ otpResend=Отправить код снова
authBack=Назад
verifyOtp=Подтвердить
otpVerifying=Проверяем...
otpSubmitUnavailable=Не удалось отправить код. Проверьте соединение и попробуйте ещё раз.
otpSubmitUnavailable=Не удалось завершить вход. Если код уже принят, закройте окно и откройте приложение.
mockMode=Тестовый режим отправки кода
phoneInvalid=Проверьте формат номера телефона.
otpInvalid=Код неверен, истёк или уже использован.
+2 -1
View File
@@ -7,9 +7,10 @@ RUN apt-get update \
COPY nginx.conf.template /etc/nginx/templates-src/nginx.conf.template
COPY templates /etc/nginx/templates-src/sites
COPY snippets /etc/nginx/snippets
COPY static /etc/nginx/static
COPY scripts/entrypoint.sh /usr/local/bin/han-nginx-entrypoint
RUN sed -i 's/\r$//' /usr/local/bin/han-nginx-entrypoint \
&& /bin/sh -n /usr/local/bin/han-nginx-entrypoint \
&& chmod 0555 /usr/local/bin/han-nginx-entrypoint \
&& find /etc/nginx/templates-src /etc/nginx/snippets -type f -exec chmod 0444 {} +
&& find /etc/nginx/templates-src /etc/nginx/snippets /etc/nginx/static -type f -exec chmod 0444 {} +
ENTRYPOINT ["/usr/local/bin/han-nginx-entrypoint"]
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>HAN Chat</title>
<style>
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 32px 24px;
background: #fff;
color: #252525;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
text-align: center;
}
p { margin: 0; line-height: 1.5; }
a {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 220px;
height: 56px;
padding: 0 24px;
border-radius: 12px;
background: #030213;
color: #fff;
font-size: 15px;
font-weight: 500;
text-decoration: none;
}
</style>
</head>
<body>
<p>Вход выполнен. Возвращаем в приложение…</p>
<p>
<a id="han-open-app" href="han-chat://auth/callbackHAN_CALLBACK_QUERY">Открыть HAN Chat</a>
</p>
<script src="/mobile/oidc/callback.js"></script>
</body>
</html>
@@ -0,0 +1,6 @@
(function () {
var target = "han-chat://auth/callback" + window.location.search + window.location.hash;
var link = document.getElementById("han-open-app");
if (link) link.href = target;
window.location.replace(target);
})();
@@ -139,6 +139,23 @@ server {
add_header Cache-Control "no-cache";
include /etc/nginx/generated/security-headers.conf;
}
location = /mobile/oidc/callback {
default_type text/html;
charset utf-8;
alias /etc/nginx/static/mobile-oidc-callback.html;
sub_filter_once on;
sub_filter_types text/html;
sub_filter HAN_CALLBACK_QUERY ?$args;
add_header Cache-Control "no-store" always;
include /etc/nginx/generated/security-headers.conf;
}
location = /mobile/oidc/callback.js {
default_type text/javascript;
charset utf-8;
alias /etc/nginx/static/mobile-oidc-callback.js;
add_header Cache-Control "no-store" always;
include /etc/nginx/generated/security-headers.conf;
}
location ^~ /auth/resources/ {
include /etc/nginx/snippets/proxy-keycloak.conf;
proxy_pass http://keycloak_upstream;
+18 -3
View File
@@ -197,14 +197,29 @@ def validate_shared(env: dict[str, str], errors: list[str]) -> None:
errors.append(f"{key}: ожидается http URL с Docker DNS service name")
if not env.get("KEYCLOAK_INTERNAL_URL", "").rstrip("/").endswith("/auth"):
errors.append("KEYCLOAK_INTERNAL_URL: внутренний URL должен заканчиваться на /auth")
if env.get("MESSAGE_SAFETY_URL", "").rstrip("/") != "https://processing.internal:8443":
safety_url = env.get("MESSAGE_SAFETY_URL", "").rstrip("/")
parsed_safety_url = urlparse(safety_url)
try:
safety_port = parsed_safety_url.port
except ValueError:
safety_port = None
safety_host = parsed_safety_url.hostname or ""
if (
parsed_safety_url.scheme != "https"
or safety_port != 8443
or not re.fullmatch(r"[A-Za-z0-9.-]+", safety_host)
or safety_host.lower() in {"message-safety", "localhost", "127.0.0.1"}
or parsed_safety_url.path
or parsed_safety_url.params
or parsed_safety_url.query
or parsed_safety_url.fragment
):
errors.append(
"MESSAGE_SAFETY_URL: ожидается remote TLS endpoint "
"https://processing.internal:8443"
"https://<private VM2 hostname>:8443, не local/stub service"
)
try:
extra_host, extra_ip = env.get("MESSAGE_SAFETY_EXTRA_HOST", "").rsplit("=", 1)
safety_host = urlparse(env.get("MESSAGE_SAFETY_URL", "")).hostname
address = ipaddress.ip_address(extra_ip)
if extra_host != safety_host or not address.is_private:
raise ValueError
@@ -152,6 +152,28 @@ class SecretHygieneTests(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertIn(expected_error, result.stderr)
def test_validator_accepts_configured_private_safety_hostname(self) -> None:
example = (ROOT / ".env.example").read_text(encoding="utf-8")
configured = (
example.replace(
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false",
"KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true",
)
.replace(
"MESSAGE_SAFETY_URL=https://processing.internal:8443",
"MESSAGE_SAFETY_URL=https://safety.vm2.corp.internal:8443",
)
.replace(
"MESSAGE_SAFETY_EXTRA_HOST=processing.internal=192.168.0.4",
"MESSAGE_SAFETY_EXTRA_HOST=safety.vm2.corp.internal=192.168.0.4",
)
)
with tempfile.TemporaryDirectory() as directory:
config = Path(directory) / ".env"
config.write_text(configured, encoding="utf-8")
result = self.run_validator(config)
self.assertEqual(result.returncode, 0, result.stderr)
if __name__ == "__main__":
unittest.main()