Как стать автором
Обновить

Как мы строили безопасное web приложение на базе WIKI.JS

Время на прочтение15 мин
Количество просмотров10K

Цель статьи - поделиться опытом, болью и поныть немного о проблемах. Никоим образом не ставилось целью показать, что именно такой путь правильный.

Как все начиналось

  • Задача - создать help раздел на базе движка wikijs.

  • Версия wikijs, которую брали за основу – 2.5.268

  • Крайняя версия, которую ставили (на момент написания статьи) – 2.5.274

Особенности и условия

  • исходный код править нельзя. Во первых, у нас в команде нет фронтендера, во вторых, непонятно как это потом поддерживать, когда будут выходить новые версии wikijs с новыми фичами

  • должны быть устранены критические уязвимости (CVE)

  • на внешнем фронте должна быть закрыта админка

  • на сборочных раннерах нет интернета, ну почти нет - некоторые ресурсы открыты через прокси по согласованию с ИБ

Основной упор был нацелен на устранение уязвимостей, а там их оказалось не мало.

Сначала было желание оповестить разработчика обо всех имеющихся критических уязвимостях, но почитав Security Policy (https://github.com/requarks/wiki/security/policy) и оценив скорость реакции на проблемы пользователей в Discussions, пришло понимание, что это плохой путь. Многие проблемы, озвученные в Discussions не решаются годами, хотя сам проект в целом активно развивается.

В Security Policy они хотят, чтобы по мимо известных CVE им присылали и примеры эксплоитов -:). Итак, было принято решение устранять уязвимости самим.

Для поиска существующих уязвимостей, использовали отчет Xray от JFrog, интегрированного с нашим docker registry.

Отчет XRAY

Для начала, надо понимать, что wikijs это гигантский комбайн с кучей всего, но весь функционал нам был не нужен, по этому, 1-й этап устранения уязвимостей – выкидывание не нужного. Каким образом удаляем не нужное – модифицируем package.json и пересобираем контейнер с приложением.

Что выкинули сразу

  1. sqlite3 – не нужно от слова совсем

  2. passport-microsoft – требуется для интеграции с azure (нам не нужно)

  3. mssql – в топку

  4. passport-azure-ad – azure нами не используется, удаляем

Следующий этап – апгрейд версий модулей, их зависимостей, а так же транзитивных зависимостей. Тут было много экспериментов. Если посмотрите файлик yarn.lock в исходном коде проекта – вам станет плохо. Там огромное количество транзитивных зависимостей. Чтобы было понимание о масштабах бедствия – этот текстовый файл имеет размер 6Mb!

Вторая большая боль – некоторые npm пакеты, используемые в данном проекте – давно не поддерживаются и имеют крайние обновления 3 – 5 лет назад, например: apollo-fetch - https://www.npmjs.com/package/apollo-fetch

А некоторые npm пакет и того пуще – deprecated, например graphql-tools версии 7.0.0 - https://www.npmjs.com/package/graphql-tools/v/7.0.0

Итак, после долгих экспериментов, что было сделано:
  1. highlight.js – заменяем на версию 10.4.1

  2. highlight.js присутствует также в зависимостях модуля diff2html. Повышаем версию diff2html до 3.4.14

  3. i18next - повышаем версию до 19.8.5

  4. js-yaml – повышаем версию до 3.14.1, также, это зависимость модуля i18next-node-fs-backend, разрешаем через resolutions: "i18next-node-fs-backend/js-yaml": "3.14.1"

  5. sqlite-libs – непонятно из каких зависимостях они берутся и в каких слоях, но для верности удаляем  целиком модуль sqlite3, так как нам он не нужен.

  6. clean-css  - повышаем до версии 5.2.2.

  7. css-what – транзитивная зависимость модуля html-webpack-plugin – повышаем до версии 5.5.0 (css-what  -> css-select -> renderkid -> pretty-error -> html-webpack-plugin)

  8. xmldom – зависимость модуля passport-saml, повышаем версию последнего до 3.1.0

  9. lodash – зависимость модуля passport-azure-ad, удаляем последний целиком

  10. node-fetch – также одна из зависимостей passport-azure-ad

  11. node-forge – транзитивная зависимость passport-saml, решаем вопрос через resolutions: "passport-saml/**/node-forge": "^1.0.0"

  12. nanoid – повышаем до версии 3.1.31

  13. ansi-regex 5.0.0 & 3.0.0 – залетают как зависимости пакетного менеджера npm вместе с образом nodejs 16.13.2. Поскольку, npm не учавствует в runtime на это можно закрыть глаза. Еще один вариант – сделать собственную сборку образа nodejs, в которую не ставить npm.

  14. ansi-regex – транзитивная зависимость eslint & cypress, решаем через механизм resolutions: "eslint/**/ansi-regex": "^5.0.1", "cypress/**/ansi-regex": "^5.0.1"

  15. underscore – зависимость пакетов express-brute & passport-cas, решаем вопрос через resolutions: "passport-cas/underscore": "^1.8.3", "express-brute/underscore": "^1.8.3"

  16. node-uuid – зависимость модуля passport-cas, разрешаем через resolutions: "passport-cas/node-uuid": "1.4.8"

  17. markdown-it – повышаем версию до 12.3.2

  18. passport-oauth2 – зависимость модуля passport-microsoft, последний не обновляется уже 2 года и имеет уже крайнюю версию – удаляем целиком из package.json (нам не требуется)

  19. ansi-regex 2.1.1 – транзитивная зависимость mssql (ansi-regex -> strip-ansi -> gauge -> npmlog -> prebuild-install -> keytar -> @azure/identity -> tedious -> mssql), удаляем целиком из package.json

Что не удалось

Дополнительные, транзитивные зависимости node-fetch -> apollo-fetch & cross-fetch -> graphql-tools. Модули apollo-fetch & cross-fetch давно не обновлялись (5 лет назад последний раз и имеют крайнюю версию). Попытка изменить версию node-fetch для apollo-fetch, приводит к ошибке.

error: require() of ES Module /wiki/node_modules/apollo-fetch/node_modules/node-fetch/src/index.js 
from /wiki/node_modules/apollo-fetch/node_modules/cross-fetch/dist/node.js not supported. 
Instead change the require of index.js in /wiki/node_modules/apollo-fetch/node_modules/cross-fetch/dist/node.js 
to a dynamic import() which is available in all CommonJS modules.

Модули ansi-regex версий 5.0.0 & 3.0.0 - залетают как зависимости пакетного менеджера npm вместе с образом nodejs 16.13.2. Поскольку, npm не участвует в runtime, на это можно закрыть глаза. Еще один вариант – сделать собственную сборку образа nodejs, в которую не ставить npm.

/usr/local/lib/node_modules/npm/node_modules/string-width/node_modules/ansi-regex
/usr/local/lib/node_modules/npm/node_modules/ansi-regex
/usr/local/lib/node_modules/npm/node_modules/cli-table3/node_modules/ansi-regex
/usr/local/lib/node_modules/npm/node_modules/cli-columns/node_modules/ansi-regex

В модулях самого приложения, ansi-regex требуемой версии:

bash-5.1$ grep 'version' /wiki/node_modules/ansi-regex/package.json
        "version": "5.0.1",

Ошибки в процессе сборки

warning Error running install script for optional dependency: "/wiki/node_modules/cpu-features: Command failed.  
Exit code: 1  
Command: node-gyp rebuild  
Arguments:  
Directory: /wiki/node_modules/cpu-features  
Output:  
gyp info it worked if it ends with ok  
gyp info using node-gyp@8.3.0  
gyp info using node@16.13.2 | linux | x64  
gyp ERR! find Python

Это ошибки сборки некоторых опциональных зависимостей и это можно игнорировать.

info This module is OPTIONAL, you can safely ignore this error

Интерпретатор python не установлен в контейнере намеренно, о чем указывает вышеописанная ошибка: "gyp ERR! find Python". В противном случае, будет осуществлена попытка сборки опциональных зависимостей из исходников, а это приведет к ошибке в пайплайне, так как будет попытка скачать исходники с разных ресурсов в интернете, а интернета на раннерах нет.

Изменения в package.json

diff2html 3.4.14

i18next 19.8.5

js-yaml 3.14.1

sqlite3 5.0.2

chalk 4.1.2

passport-microsoft 0.1.0

mssql 6.2.3 - remove

passport-saml 3.1.0

highlight.js 10.4.1

passport-azure-ad 4.3.1

html-webpack-plugin 5.5.0

cheerio 1.0.0-rc.10

nanoid 3.1.31

markdown-it 12.3.2

clean-css 5.2.2

resolutions:

    "i18next-node-fs-backend/js-yaml": "3.14.1"

    "passport-cas/node-uuid": "1.4.8",

    "passport-cas/underscore": "^1.8.3",

    "express-brute/underscore": "^1.8.3",

    "eslint/**/ansi-regex": "^5.0.1",

    "cypress/**/ansi-regex": "^5.0.1

    "passport-saml/**/node-forge": "^1.0.0"

На какие поля еще стоит обратить внимание в package.json

"version":

"2.5.274",

"releaseDate"

"2022-01-29T18:45:51.000Z",

"dev"

false,

Версию релиза и дату необходимо будет поддерживать самим. Если в поле dev будет true - увидите сверху красную полосу в UI, что приложение находится в "Development mode".

Чего делать нельзя!

Апгрейдить версию chalk до мажорной 5 и выше

Error [ERR_REQUIRE_ESM]: require() of ES Module /wiki/node_modules/chalk/source/index.js from /wiki/server/core/config.js not supported.
Instead change the require of index.js in /wiki/server/core/config.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (/wiki/server/core/config.js:2:15)
    at Object.<anonymous> (/wiki/server/index.js:17:14) {
  code: 'ERR_REQUIRE_ESM'
}

Суть проблемы

Chalk 5 has changed to ESM. They provide a link to better understand what that means: Pure ESM. From chalk README: IMPORTANT: Chalk 5 is ESM. If you want to use Chalk with TypeScript or a build tool, you will probably want to use Chalk 4 for now.

Решение - понизить версию chalk до крайней мажорной ветки 4 - 4.1.2

Повышать версию js-yaml до 4-й мажорной и выше

>>> Unable to read configuration file! Did you create the config.yml file?

    try {
      appconfig = yaml.safeLoad(
        cfgHelper.parseConfigValue(
          fs.readFileSync(confPaths.config, 'utf8')
        )
      )

Решение без правки исходного кода - не повышать версию js-yaml до 4-й мажорной ветки. Крайняя версия в ветке 3 - 3.14.1

Решение с правкой исходных кодов - менять в исходниках appconfig = yaml.safeLoad на appconfig = yaml.Load

Повышать версию graphql-rate-limit-directive выше 1.3.0

2022-01-13T19:28:06.185Z [MASTER] info: Loading GraphQL Schema...
2022-01-13T19:28:06.189Z [MASTER] error: createRateLimitTypeDef is not a function
const { createRateLimitTypeDef } = require('graphql-rate-limit-directive')
// const { GraphQLUpload } = require('graphql-upload')

Решение - понизить версию graphql-rate-limit-directive до 1.2.1

Повышать версию graphql-tools выше 7.0.0

2022-01-13T20:02:47.802Z [MASTER] info: Loading GraphQL Schema...
This package has been deprecated and now it only exports makeExecutableSchema.
And it will no longer receive updates.
We recommend you to migrate to scoped packages such as @graphql-tools/schema, @graphql-tools/utils and etc.
Check out https://www.graphql-tools.com to learn what package you should use instead!
2022-01-13T20:02:48.574Z [MASTER] error: Class extends value undefined is not a constructor or null

Решение - не повышать версию graphql-tools выше 7.0.0

Апгрейдить версию apollo-server-express

2022-01-13T20:36:57.268Z [MASTER] error: You must `await server.start()` before 
calling `server.applyMiddleware()`

Описание проблемы: This is a known bug with an open issue and a merged PR to fix it. For now, you can downgrade to apollo-server-express@^2 Пруфлинк

Решение - понижение версии apollo-server-express до 2.25.2

Одновременно ставить несколько модулей graphql разных версий, например через dependency & resolutions

2022-01-13T21:15:46.283Z [MASTER] error: Cannot use GraphQLObjectType "AnalyticsQuery" from another module or realm.

Ensure that there is only one instance of "graphql" in the node_modules
directory. If different versions of "graphql" are the dependencies of other
relied on modules, use "resolutions" to ensure only one version is installed.

https://yarnpkg.com/en/docs/selective-version-resolutions

Duplicate "graphql" modules cannot be used at the same time since different
versions may have different capabilities and behavior. The data from one
version used in the function from another could produce confusing and
spurious results.

Решение - в package.json в dependency & resolutions должны быть одинаковые версии модуля graphql

Апгрейдить модуль graphql до 16-й мажорной ветки

2022-01-13T21:49:51.999Z [MASTER] error: graphql@16 dropped long-deprecated support 
for positional arguments, please pass an object instead.

Решение - понижение версии модуля graphql до 15.3.0

Как строили пайплайн со сборочным конвейером

Примерный план

  • клонирование проекта с исходным кодом и переход на ветку с номером релиза

  • скачивание архива со сбилдженным приложением. Из данного архива берутся assets & server/views.

  • формирование .npmrc & .yarnrc для подключения remote-npm

  • скачивание пакетов локализации из проекта wiki-localization.git

Особенности реализации

  • Сборка осуществляется на раннерах гитлаба с подключением удаленного репозитория npm (remote-npm).

  • Этап build (yarn build) исключен из докерфайла, так как требует сборки некоторых модулей из исходников и соответственно, для этого нужен интернет.

  • Артифакты этапа build - assets, копируются из архива, официально сбилдженного приложения из github.

  • В контейнер с приложением, дополнительно копируются файлы локализации из проекта wiki-localization.git (https://github.com/Requarks/wiki-localization)

  • Файл конфига немного модифицировали (config.yml)

config.yml
port: 3000
bindIP: 0.0.0.0
db:
  type: $(DB_TYPE)
  host: '$(DB_HOST)'
  port: $(DB_PORT)
  user: '$(DB_USER)'
  pass: '$(DB_PASS)'
  db: $(DB_NAME)
  storage: $(DB_FILEPATH)
  ssl: $(DB_SSL)
logLevel: $(LOGLEVEL)
ha: $(HA_ACTIVE)
offline: true

Описание процесса сборки

В качестве базового брали образ nodejs 16.13.2 alpine 3.15.

Стейдж build

  • Установка yarn

  • Копирование модифицированного package.json из данного проекта в рабочую директорию

  • Копирование .npmrc & .yarnrc в рабочую директорию

  • Сборка модулей и их зависимостей из списка dependencies

Стейдж release

  • Установка всех необходимых пакетов

  • Создание директорий

  • Копирование собранных модулей из стейджа build

  • Копирование папки servers из проекта с исходным кодом

  • Копирование assets & server/views из архива с официальной сборкой проекта

  • Копирование package.json

  • Копирование конфигурационного файла приложения config.yml

  • Копирование лицензионного соглашение (из офиц релиза)

  • Копирование файлов локализации

Dockerfile под катом
# ====================
# --- Build Assets ---
# ====================
FROM artifactory.domain.ru/docker/node:16.13.2-alpine3.15 AS build
RUN apk add yarn --no-cache

WORKDIR /wiki

COPY ./package.json ./package.json
COPY ./.yarnrc ./.yarnrc
COPY ./.npmrc ./.npmrc

RUN yarn cache clean
RUN yarn --frozen-lock --production --non-interactive --verbose

# ===============
# --- Release ---
# ===============
FROM artifactory.domain.ru/docker/node:16.13.2-alpine3.15 as release
LABEL ru.domain.wikijs.title="Open source Wiki software based on Node.js" \
      ru.domain.wikijs.description="Updated image for Wiki.js." \
      ru.domain.wikijs.responsible="someone@domain.ru"

RUN apk add --no-cache -u \
        bash \
        curl \
        git \
        openssh \
        gnupg && \
    mkdir -p /wiki && \
    mkdir -p /logs && \
    mkdir -p /wiki/data/sideload && \
    chown -R node:node /wiki /logs

WORKDIR /wiki

COPY --chown=node:node ./assets ./assets
COPY --chown=node:node ./manifest.json ./assets/manifest.json
COPY --chown=node:node --from=build /wiki/node_modules ./node_modules
COPY --chown=node:node ./server ./server
COPY --chown=node:node ./server/views ./server/views
COPY --chown=node:node ./config.yml ./config.yml
COPY --chown=node:node ./package.json ./package.json
COPY --chown=node:node ./LICENSE ./LICENSE
COPY --chown=node:node ./wiki-localization/locales.json ./data/sideload/locales.json
COPY --chown=node:node ./wiki-localization/en.json ./data/sideload/en.json
COPY --chown=node:node ./wiki-localization/ru.json ./data/sideload/ru.json

USER node

EXPOSE 3000

CMD ["node", "server"]

На этом процесс сборки контейнера с приложением закончен.

Второй этап - доработка напильником

Для того чтобы запретить любую возможность входа в административную панель приложения снаружи, было принято решение поднять 2 фронта. Примерно это выглядит так:

  • внутренний фронт с админкой - крутится в корпоративной сети (одна реплика)

  • внешний фронт без админки - торчит наружу (2 реплики)

  • оба фронта смотрят в один бэк - в одну БД (HA Cluster Patroni)

Как закрывали админку на внешнем фронте – закрытием локаций с использованием nginx на ingress контроллере кубера, где деплоили приложение. Пример:

nginx.ingress.kubernetes.io/server-snippet: |
  location ~* "^/(login|_userav|h|u|e|logout|register|verify|login-reset|\.well-known|healthz)" {
     return 301 https://$server_name;
        }

Вторая часть задачи по закрытию возможности авторизации - выпиливание кнопки авторизации с панели управления, удалением куска js кода с использованием subs_filter nginx на том же ingress контроллере. Пример:

nginx.ingress.kubernetes.io/configuration-snippet: |
      proxy_set_header Accept-Encoding "";
      subs_filter "return\[[^\]]+?href:\"\/login[^\]]+?\]\)\][,]1\)\]" "return[]" ro;
      subs_filter_types text/html text/javascript application/javascript;

Что еще делали – онлайн замену реальных пользователей, на обезличенного Administrator. Эта инфа отображается на каждой страничках wiki – Дата изменения и username. Тут так же - subs_filter от nginx.

subs_filter "author-name=\"(.*?)\"" "author-name=\"Administrator\"" ro;
subs_filter "author-id=\"(\d+)\"" "author-id=\"0\"" ro;

Замена favicon, так же, через грязный хак. Ну нельзя штатными средствами это сделать.

subs_filter "(<meta name=\"msapplication-TileImage\" content=\"\/)_assets\/favicons\/mstile-150x150\.png(\">)" "$1new__favicon.ico$2" ro;
subs_filter "<link(.+?)assets\/favicons\/(.+?)>" "" rg;

Безопасность должна быть безопасной!

Наши апсеки узрели дырку в авторизации на локейшене /graphl. Целиком данный локейш выпилить нельзя - на нем реализован поиск контента. Пришлось применить lua скрипт по ограничению методов с просмотром тела запросов. Спасибо коллегам за помощь!

Магия nginx lua
location /graphql {
            set $namespace      "{{ namespace }}";
            set $ingress_name   "{{ app.name }}-ingress";
            set $service_name   "{{ app.name }}";
            set $service_port   "80";
            set $location_path  "/graphql";
            set $global_rate_limit_exceeding n;

            limit_except POST {
                deny all;
              }

            rewrite_by_lua_block {
              lua_ingress.rewrite({
                force_ssl_redirect = false,
                ssl_redirect = true,
                force_no_ssl_redirect = false,
                use_port_in_redirects = false,
              global_throttle = { namespace = "", limit = 0, window_size = 0, key = { }, ignored_cidrs = { } },
              })
              balancer.rewrite()
              plugins.run()

              ngx.req.read_body()
              local body = ngx.req.get_body_data()

              if body then
                  local m = ngx.re.match(body, "(search|pages|localization)")
                  if m==nil then body="" end

                  body = ngx.re.gsub(body, "authentication", "wrong")
                  body = ngx.re.gsub(body, "mutation", "wrong")
              end
              ngx.req.set_body_data(body)
            }

            # be careful with `access_by_lua_block` and `satisfy any` directives as satisfy any
            # will always succeed when there's `access_by_lua_block` that does not have any lua code doing `ngx.exit(ngx.DECLINED)`
            # other authentication method such as basic auth or external auth useless - all requests will be allowed.
            #access_by_lua_block {
            #}

            header_filter_by_lua_block {
              lua_ingress.header()
              plugins.run()
            }

            body_filter_by_lua_block {
              plugins.run()
            }

            log_by_lua_block {
              balancer.log()

              monitor.call()

              plugins.run()
            }

            port_in_redirect off;

            set $balancer_ewma_score -1;
            set $proxy_upstream_name "service-wikijs-80";
            set $proxy_host          $proxy_upstream_name;
            set $pass_access_scheme  $scheme;

            set $pass_server_port    $server_port;

            set $best_http_host      $http_host;
            set $pass_port           $pass_server_port;

            set $proxy_alternative_upstream_name "";

            client_max_body_size                    1m;

            proxy_set_header Host                   $best_http_host;

            # Pass the extracted client certificate to the backend

            # Allow websocket connections
            proxy_set_header                        Upgrade           $http_upgrade;

            proxy_set_header                        Connection        $connection_upgrade;

            proxy_set_header X-Request-ID           $req_id;
            proxy_set_header X-Real-IP              $remote_addr;

            proxy_set_header X-Forwarded-For        $remote_addr;

            proxy_set_header X-Forwarded-Host       $best_http_host;
            proxy_set_header X-Forwarded-Port       $pass_port;
            proxy_set_header X-Forwarded-Proto      $pass_access_scheme;

            proxy_set_header X-Scheme               $pass_access_scheme;

            # Pass the original X-Forwarded-For
            proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;

            # mitigate HTTPoxy Vulnerability
            # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
            proxy_set_header Proxy                  "";

            # Custom headers to proxied server

            proxy_connect_timeout                   5s;
            proxy_send_timeout                      60s;
            proxy_read_timeout                      60s;

            proxy_buffering                         off;
            proxy_buffer_size                       4k;
            proxy_buffers                           4 4k;

            proxy_max_temp_file_size                1024m;

            proxy_request_buffering                 on;
            proxy_http_version                      1.1;

            proxy_cookie_domain                     off;
            proxy_cookie_path                       off;

            # In case of errors try the next upstream server before returning an error
            proxy_next_upstream                     error timeout;
            proxy_next_upstream_timeout             0;
            proxy_next_upstream_tries               3;

            proxy_pass http://upstream_balancer;

            proxy_redirect                          off;
      }

Интеграция с AD

Тут было несколько проблем

  • LDAP URL - Если указать ldaps://domain.ru:636, то возникала ошибка невозможности отрезолвить данный домен, при этом, сеть внутри контейнера работала нормально. Проблема в каком-то js модуле или методе. Ошибка воспроизводится 100% только в docker контейнере. Пришлось указывать тут IP PDC.

  • Проблема в понимании секции "Разрешить самостоятельную регистрацию/self-registration (https://docs.requarks.io/auth/ldap). Если не включать данную опцию и не прописывать «Назначить группу», то при авторизации нового пользователя получим «тыкву». Дело в том, что для полноценной работы, нужны локальные пользователи и локальная группа, которой будут даны определенные права. Если включить данную опцию, то при успешной LDAP авторизации, создадутся новые локальные пользователи, с правами группы, которая указана в данной настройке. Пользователи создадутся с именем = значению поля в AD, указанным в "Display Name Field Mapping". Либо необходимо заранее создать локальных пользователей, назначить им права, при этом имена пользователей должны совпадать со значением поля в AD указанного в "Display Name Field Mapping"

Известные проблемы

  1. Приложение может быть размещено только на выделенном домене, например wiki.domain.com и не может быть размещено в subpath, т.е. www.domain.com/wiki Пруф https://github.com/Requarks/wiki/discussions/4656

  2. Приложение пытается лезть в интернет, для скачки файлов локализации, по этому, если интернета нет на сервере, скачиваем пакеты локализации, подкладываем приложению (описано в dockerfile), а в конфиге config.yml прописываем offline: true

  3. Невозможно штатными средствами заменить favicon (применяли вышеуказанный хак). Обсуждение https://github.com/Requarks/wiki/discussions/3371, https://js.wiki/feedback/p/favicon

  4. Нельзя удалить/переименовать директории для медиафайлов (прям рука-лицо) https://feedback.js.wiki/wiki/p/delete-folders-in-the-image-file-manager

  5. Нельзя глобально изменить дефолтный timezone (тут тоже рука-лицо). Пруф https://feedback.js.wiki/wiki/p/select-date-format-and-time-zone-for-the-entire-site. Пока только такой грязный хак: ALTER TABLE users ALTER COLUMN timezone SET DEFAULT 'Europe/Moscow '; По умолчанию пользователь получает timezone GMT-3 или что-то в этом духе.

  6. Настройка через текстовый конфиг невозможна. Например, настройка интеграции с LDAP, настройка локализации, создание групп, пользователей и т.д. Настройка предполагается только через UI. Автоматизация? Не, не слышали. Ну либо применять вливание настроек через sql скрипт.

  7. Некоторые вещи, например таблица стилей css, попадает в локальный файловый кэш на момент старта приложения. Этот кэш обновляется при изменении и сохранении контента страниц. Если несколько инстансов приложения (например несколько реплик в кубернетес для HA), то могут возникать такие коллизии - при логине в админку попадаешь на одно приложение в рамках сессии и после обновления контента, локальный кэш обновляется только у одного приложения. Далее если перейти на данный ресурс в браузере и понажимать F5 для обновления страницы, попадая на второй инстанс приложения, можно увидеть различия в оформлении, если оно менялось. Механизма управления локальным кэшем нет. По этому, сделали авторестарт внешних фронтов по расписанию (ночью).

Спрашивайте, может чего еще вспомню. А пока, пойду помою руки после nodejs.

Всем добра! -:)

Теги:
Хабы:
+1
Комментарии4

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн