refactor: сертификат и ключ в каталоге secrets/, один параметр на каталог
Tests / frontend-test (pull_request) Successful in 3m14s
Tests / frontend-test (pull_request) Successful in 3m14s
Локально путь к сертификату вёл в ~/Downloads у каждого свой, а в контейнере задавался двумя отдельными переменными. Теперь каталог один и подменяется целиком через SECRETS_DIR: по умолчанию secrets/ в корне репозитория, в контейнере /certs (смонтированный каталог с хоста, GRAPH_CERT_DIR не меняется). Имена файлов внутри фиксированы, поэтому GRAPH_CERT_PATH и GRAPH_KEY_PATH больше не нужны. Содержимое secrets/ в .gitignore, кроме README. Отсутствие файлов теперь даёт понятную ошибку вместо ENOENT — на свежем клоне это основной сценарий отказа. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,3 +15,8 @@ yarn-error.log*
|
|||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Сертификаты и ключи. Каталог подменяется целиком: локально это secrets/,
|
||||||
|
# в контейнере — примонтированный /certs (см. docker-compose.yml).
|
||||||
|
secrets/*
|
||||||
|
!secrets/README.md
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ npm run dev
|
|||||||
так же, как nginx в проде (`nginx.conf`). Без запущенного `[auth]` отправка
|
так же, как nginx в проде (`nginx.conf`). Без запущенного `[auth]` отправка
|
||||||
формы отдаёт 500 от прокси.
|
формы отдаёт 500 от прокси.
|
||||||
|
|
||||||
Сервису нужен `server/.env` с сертификатом Azure AD — скопируйте
|
Сервису нужны две вещи: `server/.env` (скопируйте
|
||||||
[server/.env.example](server/.env.example) и подставьте свои пути. Сами
|
[server/.env.example](server/.env.example)) и сертификат Azure AD в каталоге
|
||||||
`.pem`-файлы в git не хранятся, их надо взять у администратора. Подробнее —
|
[`secrets/`](secrets/README.md). Сами `.pem`-файлы в git не хранятся — их надо
|
||||||
в [server/README.md](server/README.md).
|
взять у администратора. Подробнее — в [server/README.md](server/README.md).
|
||||||
|
|
||||||
Прочие команды:
|
Прочие команды:
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -13,8 +13,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
GRAPH_TENANT_ID: ${GRAPH_TENANT_ID:?в .env на сервере не задан GRAPH_TENANT_ID}
|
GRAPH_TENANT_ID: ${GRAPH_TENANT_ID:?в .env на сервере не задан GRAPH_TENANT_ID}
|
||||||
GRAPH_CLIENT_ID: ${GRAPH_CLIENT_ID:?в .env на сервере не задан GRAPH_CLIENT_ID}
|
GRAPH_CLIENT_ID: ${GRAPH_CLIENT_ID:?в .env на сервере не задан GRAPH_CLIENT_ID}
|
||||||
GRAPH_CERT_PATH: /certs/SPFxDeploy_cert.pem
|
# Один параметр на весь каталог с секретами — имена файлов внутри
|
||||||
GRAPH_KEY_PATH: /certs/SPFxDeploy_key.pem
|
# фиксированы (см. server/src/config.ts).
|
||||||
|
SECRETS_DIR: /certs
|
||||||
volumes:
|
volumes:
|
||||||
# Каталог с сертификатом/ключом на сервере — путь задаётся в .env
|
# Каталог с сертификатом/ключом на сервере — путь задаётся в .env
|
||||||
# (GRAPH_CERT_DIR), сами файлы в git не попадают.
|
# (GRAPH_CERT_DIR), сами файлы в git не попадают.
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# secrets
|
||||||
|
|
||||||
|
Сертификат и приватный ключ приложения Azure AD, под которым сервис
|
||||||
|
`hr-appeals-auth` получает app-only токен. **В git не попадают** — всё
|
||||||
|
содержимое каталога, кроме этого файла, в `.gitignore`.
|
||||||
|
|
||||||
|
Положите сюда два файла (взять у администратора):
|
||||||
|
|
||||||
|
```
|
||||||
|
secrets/
|
||||||
|
SPFxDeploy_cert.pem
|
||||||
|
SPFxDeploy_key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
Имена фиксированные — их ждёт [server/src/config.ts](../server/src/config.ts).
|
||||||
|
|
||||||
|
## Подменить каталог целиком
|
||||||
|
|
||||||
|
Путь задаётся одной переменной `SECRETS_DIR`, поэтому каталог можно заменить
|
||||||
|
не трогая код:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SECRETS_DIR=/path/to/other/secrets npm run dev --prefix server
|
||||||
|
```
|
||||||
|
|
||||||
|
По умолчанию — этот каталог. В продовом контейнере `SECRETS_DIR=/certs`, куда
|
||||||
|
смонтирован каталог с хоста: путь на хосте задаёт `GRAPH_CERT_DIR` в `.env`
|
||||||
|
рядом с `docker-compose.yml` на сервере.
|
||||||
|
|
||||||
|
## Если ключ утёк
|
||||||
|
|
||||||
|
Сертификат перевыпускается в Azure AD (App registrations → нужное приложение →
|
||||||
|
Certificates & secrets), старый оттуда удаляется. Замена файлов здесь без этого
|
||||||
|
ничего не даёт: Azure AD продолжит принимать подпись старым ключом.
|
||||||
+6
-6
@@ -1,13 +1,13 @@
|
|||||||
# Локальный запуск: скопируйте в server/.env и подставьте свои пути к сертификату.
|
# Локальный запуск: скопируйте в server/.env.
|
||||||
# На проде эти переменные задаёт docker-compose (см. docker-compose.yml), .env там свой.
|
# На проде эти переменные задаёт docker-compose (см. docker-compose.yml).
|
||||||
|
|
||||||
GRAPH_TENANT_ID=b4c2af35-bfd6-4cff-9af9-556efb53e65a
|
GRAPH_TENANT_ID=b4c2af35-bfd6-4cff-9af9-556efb53e65a
|
||||||
GRAPH_CLIENT_ID=26d2e058-0b27-4a00-b49f-1f17c4eecd1d
|
GRAPH_CLIENT_ID=26d2e058-0b27-4a00-b49f-1f17c4eecd1d
|
||||||
|
|
||||||
# Сертификат и приватный ключ приложения — в git не хранятся, взять у администратора.
|
|
||||||
GRAPH_CERT_PATH=C:/path/to/SPFxDeploy_cert.pem
|
|
||||||
GRAPH_KEY_PATH=C:/path/to/SPFxDeploy_key.pem
|
|
||||||
|
|
||||||
# 3000 занят vite (см. vite.config.ts), поэтому локально сервис слушает 3100 —
|
# 3000 занят vite (см. vite.config.ts), поэтому локально сервис слушает 3100 —
|
||||||
# именно туда проксирует /api из dev-сервера.
|
# именно туда проксирует /api из dev-сервера.
|
||||||
PORT=3100
|
PORT=3100
|
||||||
|
|
||||||
|
# Каталог с сертификатом и ключом. По умолчанию — secrets/ в корне репозитория,
|
||||||
|
# задавать нужно только чтобы подменить его на другой (см. secrets/README.md).
|
||||||
|
# SECRETS_DIR=/path/to/secrets
|
||||||
|
|||||||
+5
-3
@@ -13,8 +13,7 @@ assertion) и используется для авторизованных за
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `GRAPH_TENANT_ID` | Tenant ID Azure AD |
|
| `GRAPH_TENANT_ID` | Tenant ID Azure AD |
|
||||||
| `GRAPH_CLIENT_ID` | Client ID App Registration, к которому привязан сертификат `SPFxDeploy` |
|
| `GRAPH_CLIENT_ID` | Client ID App Registration, к которому привязан сертификат `SPFxDeploy` |
|
||||||
| `GRAPH_CERT_PATH` | Путь к `SPFxDeploy_cert.pem` внутри контейнера (см. `docker-compose.yml` в корне репозитория) |
|
| `SECRETS_DIR` | Каталог с `SPFxDeploy_cert.pem` и `SPFxDeploy_key.pem` (имена файлов фиксированы). По умолчанию — [`secrets/`](../secrets/README.md) в корне репозитория; в контейнере `/certs`, куда смонтирован каталог с хоста. Задавать нужно только чтобы подменить каталог целиком |
|
||||||
| `GRAPH_KEY_PATH` | Путь к `SPFxDeploy_key.pem` внутри контейнера |
|
|
||||||
| `GRAPH_SCOPE` | OAuth-scope запрашиваемого токена. По умолчанию — App ID URI самого `api.tnsu.kz` (`api://9232d9e2-.../.default`), задавать не нужно. Токен для `https://graph.microsoft.com/.default` тут не подойдёт: `api.tnsu.kz` проверяет `aud` входящего JWT и отвечает 401 |
|
| `GRAPH_SCOPE` | OAuth-scope запрашиваемого токена. По умолчанию — App ID URI самого `api.tnsu.kz` (`api://9232d9e2-.../.default`), задавать не нужно. Токен для `https://graph.microsoft.com/.default` тут не подойдёт: `api.tnsu.kz` проверяет `aud` входящего JWT и отвечает 401 |
|
||||||
| `PORT` | Порт HTTP-сервера сервиса, по умолчанию `3000` (локально — `3100`, см. `.env.example`) |
|
| `PORT` | Порт HTTP-сервера сервиса, по умолчанию `3000` (локально — `3100`, см. `.env.example`) |
|
||||||
|
|
||||||
@@ -23,10 +22,13 @@ assertion) и используется для авторизованных за
|
|||||||
Из корня репозитория `npm run dev` поднимает сервис вместе с фронтендом. Отдельно:
|
Из корня репозитория `npm run dev` поднимает сервис вместе с фронтендом. Отдельно:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # подставить свои пути к SPFxDeploy_cert.pem / _key.pem
|
cp .env.example .env # tenant/client id и порт
|
||||||
npm run dev # tsc + запуск с чтением .env (--env-file, Node 20+)
|
npm run dev # tsc + запуск с чтением .env (--env-file, Node 20+)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Сертификат и ключ при этом должны лежать в [`secrets/`](../secrets/README.md) —
|
||||||
|
в git они не хранятся.
|
||||||
|
|
||||||
Проверить, что авторизация работает: `curl http://localhost:3100/diag/token` →
|
Проверить, что авторизация работает: `curl http://localhost:3100/diag/token` →
|
||||||
`{"ok":true}`. Если вернулась ошибка — в ней будет ответ Azure AD целиком.
|
`{"ok":true}`. Если вернулась ошибка — в ней будет ответ Azure AD целиком.
|
||||||
|
|
||||||
|
|||||||
+14
-2
@@ -1,3 +1,5 @@
|
|||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
|
||||||
function requireEnv(name: string): string {
|
function requireEnv(name: string): string {
|
||||||
const value = process.env[name];
|
const value = process.env[name];
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -6,11 +8,21 @@ function requireEnv(name: string): string {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Каталог с сертификатом и приватным ключом приложения. Одна переменная на весь
|
||||||
|
* каталог, а не путь к каждому файлу: так его можно подменить централизованно —
|
||||||
|
* локально это `secrets/` в корне репозитория (в git не попадает), в контейнере
|
||||||
|
* SECRETS_DIR=/certs, куда смонтирован каталог с хоста (см. docker-compose.yml).
|
||||||
|
*
|
||||||
|
* __dirname здесь — server/dist, поэтому до корня репозитория два уровня вверх.
|
||||||
|
*/
|
||||||
|
const secretsDir = process.env.SECRETS_DIR ?? resolve(__dirname, '../../secrets');
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
tenantId: requireEnv('GRAPH_TENANT_ID'),
|
tenantId: requireEnv('GRAPH_TENANT_ID'),
|
||||||
clientId: requireEnv('GRAPH_CLIENT_ID'),
|
clientId: requireEnv('GRAPH_CLIENT_ID'),
|
||||||
certPath: requireEnv('GRAPH_CERT_PATH'),
|
certPath: join(secretsDir, 'SPFxDeploy_cert.pem'),
|
||||||
keyPath: requireEnv('GRAPH_KEY_PATH'),
|
keyPath: join(secretsDir, 'SPFxDeploy_key.pem'),
|
||||||
// Токен нужен не для Graph напрямую, а для api.tnsu.kz — он сам ходит в
|
// Токен нужен не для Graph напрямую, а для api.tnsu.kz — он сам ходит в
|
||||||
// SharePoint. Это его App ID URI: с graph.microsoft.com/.default api вернёт
|
// SharePoint. Это его App ID URI: с graph.microsoft.com/.default api вернёт
|
||||||
// 401, потому что проверяет aud во входящем JWT.
|
// 401, потому что проверяет aud во входящем JWT.
|
||||||
|
|||||||
+17
-2
@@ -21,9 +21,24 @@ const REFRESH_MARGIN_MS = 60_000;
|
|||||||
|
|
||||||
let cached: CachedToken | null = null;
|
let cached: CachedToken | null = null;
|
||||||
|
|
||||||
|
function readSecret(path: string): string {
|
||||||
|
try {
|
||||||
|
return readFileSync(path, 'utf8');
|
||||||
|
} catch (cause) {
|
||||||
|
// Самая частая причина на свежем клоне — файлов просто нет: в git они не
|
||||||
|
// хранятся. Ошибка ENOENT сама по себе не подсказывает, что делать.
|
||||||
|
throw new Error(
|
||||||
|
`Не удалось прочитать ${path}. Сертификат и ключ в git не хранятся — ` +
|
||||||
|
'положите их в каталог secrets/ (см. secrets/README.md) или укажите ' +
|
||||||
|
'другой каталог через SECRETS_DIR.',
|
||||||
|
{ cause }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildClientAssertion(): string {
|
function buildClientAssertion(): string {
|
||||||
const certificatePem = readFileSync(config.certPath, 'utf8');
|
const certificatePem = readSecret(config.certPath);
|
||||||
const privateKey = readFileSync(config.keyPath, 'utf8');
|
const privateKey = readSecret(config.keyPath);
|
||||||
|
|
||||||
// x5t — SHA-1 отпечаток DER-сертификата, base64url. По нему Azure AD находит
|
// x5t — SHA-1 отпечаток DER-сертификата, base64url. По нему Azure AD находит
|
||||||
// публичный ключ приложения и проверяет подпись JWT.
|
// публичный ключ приложения и проверяет подпись JWT.
|
||||||
|
|||||||
Reference in New Issue
Block a user